Skip to content

Commit

Permalink
- fixed api endpoint that creates admin user in IdentityEndpoints.cs …
Browse files Browse the repository at this point in the history
…& UserService.cs

- fixed method that call onboarding endpoints in frontend in ApiService.cs
- removed default functionality to create seed users in DatabaseInitializer.cs
- created another way to send notifications in NotificationService.cs
- finished onboarding in general
- updated project ProjectConstants.cs
  • Loading branch information
m-GDEV committed Nov 27, 2024
1 parent 62dd28a commit e09476c
Show file tree
Hide file tree
Showing 15 changed files with 294 additions and 55 deletions.
42 changes: 0 additions & 42 deletions WardrobeManager.Api/Database/DatabaseInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,47 +39,5 @@ public static async Task InitializeAsync(IServiceScope scope)
}
}

// User Creation
foreach (var user in seedUsers)
{
var hashed = password.HashPassword(user, "password123");
user.PasswordHash = hashed;
await userStore.CreateAsync(user);

if (user.Email is not null)
{
var appUser = await userManager.FindByEmailAsync(user.Email);

if (appUser is not null && user.RoleList is not null)
{
await userManager.AddToRolesAsync(appUser, user.RoleList);
}
}
}

}

private class SeedUser : AppUser
{
public string[]? RoleList { get; set; }
}
private static readonly IEnumerable<SeedUser> seedUsers =
[
new SeedUser()
{
Email = "[email protected]",
NormalizedEmail = "[email protected]",
NormalizedUserName = "[email protected]",
RoleList = [ "Admin", "User" ],
UserName = "[email protected]"
},
new SeedUser()
{
Email = "[email protected]",
NormalizedEmail = "[email protected]",
NormalizedUserName = "[email protected]",
RoleList = [ "User" ],
UserName = "[email protected]"
},
];
}
33 changes: 29 additions & 4 deletions WardrobeManager.Api/Endpoints/IdentityEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Authorization;
using System.Diagnostics;
using System.Security.Claims;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using WardrobeManager.Api.Database.Models;
Expand All @@ -18,14 +19,17 @@ public static class IdentityEndpoints
{
public static void MapIdentityEndpoints(this IEndpointRouteBuilder app)
{

app.MapPost("/logout", LogoutAsync).RequireAuthorization();
app.MapGet("/roles", RolesAsync).RequireAuthorization();

// Onboarding
app.MapGet("/does-admin-user-exist", DoesAdminUserExist);
app.MapPost("/create-admin-user-if-missing", CreateAdminIfMissing);
}

// Provide an end point to clear the cookie for logout
// For more information on the logout endpoint and antiforgery, see:
// https://learn.microsoft.com/aspnet/core/blazor/security/webassembly/standalone-with-identity#antiforgery-support
// Provide an end point to clear the cookie for logout
// For more information on the logout endpoint and antiforgery, see:
// https://learn.microsoft.com/aspnet/core/blazor/security/webassembly/standalone-with-identity#antiforgery-support
public static async Task<IResult> LogoutAsync(SignInManager<AppUser> signInManager, [FromBody] object empty)
{
if (empty is not null)
Expand Down Expand Up @@ -59,4 +63,25 @@ public static async Task<IResult> RolesAsync(ClaimsPrincipal user)

return Results.Unauthorized();
}

// These methods are called by the frontend during the onboarding process
public static async Task<IResult> DoesAdminUserExist(IUserService userService)
{
return TypedResults.Ok(await userService.DoesAdminUserExist());
}

public static async Task<IResult> CreateAdminIfMissing(IUserService userService,
[FromBody] AdminUserCredentials credentials)
{
var res = await userService.CreateAdminIfMissing(credentials.email, credentials.password);

if (res.Item1)
{
return Results.Created();
}
else
{
return Results.Conflict(res.Item2);
}
}
}
5 changes: 4 additions & 1 deletion WardrobeManager.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@
builder.Services.AddAuthorizationBuilder();

// Add identify and opt-in to endpoints
builder.Services.AddIdentityCore<AppUser>()
builder.Services.AddIdentityCore<AppUser>(options =>
{
options.User.RequireUniqueEmail = true;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<DatabaseContext>()
// this maps some Identity API endpoints like '/register' check Swagger for all of them
Expand Down
73 changes: 72 additions & 1 deletion WardrobeManager.Api/Services/Implementation/UserService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System.Diagnostics;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WardrobeManager.Api.Database;
using WardrobeManager.Api.Database.Models;
using WardrobeManager.Api.Services.Interfaces;
using WardrobeManager.Shared.Enums;
using WardrobeManager.Shared.Models;
Expand All @@ -10,10 +13,14 @@ namespace WardrobeManager.Api.Services.Implementation;
public class UserService : IUserService
{
private readonly DatabaseContext _context;
private readonly UserManager<AppUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;

public UserService(DatabaseContext databaseContext)
public UserService(DatabaseContext databaseContext, UserManager<AppUser> userManager, RoleManager<IdentityRole> roleManager)
{
_context = databaseContext;
_userManager = userManager;
_roleManager = roleManager;
}

// Auth0Id methods (used in middleware)
Expand Down Expand Up @@ -83,4 +90,68 @@ public async Task DeleteUser(int userId)
_context.Users.Remove(dbRecord);
await _context.SaveChangesAsync();
}

// These methods are called by the frontend during the onboarding process

// REVIEW: performance can likely be improved
public async Task<bool> DoesAdminUserExist()
{
var users = await _userManager.Users.ToListAsync();

var adminRoleExists = await _roleManager.RoleExistsAsync("Admin");
Debug.Assert(adminRoleExists == true, "Admin role should exist (created in db init)!");

foreach (var user in users)
{
if (await _userManager.IsInRoleAsync(user, "Admin"))
{
return true;
}
}

return false;
}

public async Task<(bool, string)> CreateAdminIfMissing(string email, string password)
{
if (await DoesAdminUserExist())
{
return (false,"Admin user already exists!");
}

var hasher = new PasswordHasher<AppUser>();

var adminRoleExists = await _roleManager.RoleExistsAsync("Admin");
Debug.Assert(adminRoleExists == true, "Admin role should exist (created in db init)!");

var user = new AppUser
{
Email = email,
NormalizedEmail = email.ToUpper(),
UserName = email.ToUpper(),
NormalizedUserName = email.ToUpper(),
};

var hashed = hasher.HashPassword(user, password);
user.PasswordHash = hashed;
var createResult = await _userManager.CreateAsync(user);

if (createResult.Succeeded is false)
{
var errors = createResult.Errors.Select(e => e.Description).ToList();
return (false, string.Join(" ", errors));
}

var roleResult = await _userManager.AddToRoleAsync(user, "Admin");

if (roleResult.Succeeded is false)
{
var errors = roleResult.Errors.Select(e => e.Description).ToList();
return (false, string.Join(" ", errors));
}

await _context.SaveChangesAsync();

return (true, "Admin user created!");
}
}
5 changes: 5 additions & 0 deletions WardrobeManager.Api/Services/Interfaces/IUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ public interface IUserService
public Task<User?> GetUser(int userId);
Task UpdateUser(int userId, EditedUserDTO editedUser);
Task DeleteUser(int userId);

public Task<bool> DoesAdminUserExist();

// bool: succeeded?, string: text description
public Task<(bool, string)> CreateAdminIfMissing(string email, string password);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="w-3/5 bg-primary-content h-full">
<div class="w-5/6 mx-auto flex flex-col items-center justify-center h-full">
<h1 class="text-primary title-text mt-8 mb-8">Wardrobe Manager</h1>
<img class="rounded-lg max-w-[32rem]" src="img/home-background.jpg" />
<img class="rounded-lg max-w-[32rem]" src="@ProjectConstants.HomeBackgroundImage" />
</div>
</div>

Expand All @@ -13,12 +13,12 @@
<h2 class="pt-8 heading-text text-primary-content">@PageHeaderText</h2>
<EditForm class="pt-8 w-full pb-8" Model="@formModel" OnSubmit="SubmitButtonMethod">
<p class="text-primary-content py-2 subtitle-text">Email</p>
<InputText @bind-Value="@formModel.email" required class=" w-full subtitle-text input input-bordered px-6 py-10 text-primary bg-primary-content mb-4 rounded-2xl" placeholder="Your Username Here" type="email"/>
<InputText @bind-Value="@formModel.email" required class=" w-full subtitle-text input input-bordered px-6 py-10 text-primary bg-primary-content mb-4 rounded-2xl" placeholder="@("[email protected]")" type="email"/>

<p class="text-primary-content py-2 subtitle-text">Password</p>
<InputText @bind-Value="@formModel.password" required class="subtitle-text w-full input input-bordered px-6 py-10 text-primary bg-primary-content mb-8 rounded-2xl" placeholder="............" type="password"/>

<input class="w-full py-4 subtitle-text bg-secondary text-secondary-content rounded-2xl " type="submit" value="@SubmitButtonText"/>
<input class="btn btn-secondary btn-wide w-full subtitle-text rounded-2xl " type="submit" value="@SubmitButtonText"/>
</EditForm>

<div class="flex flex-row items-center gap-3 mb-8">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@namespace WardrobeManager.Presentation.Components.Onboarding

<div class="grow w-full flex flex-col items-center gap-5 justify-evenly">
<h3 class="subheading-text text-primary">@Title</h3>
@ChildContent
<button @onclick="ButtonClickCallback" class="btn btn-accent btn-wide subtitle-text">@ButtonText</button>
</div>

@code {
[Parameter] public required string Title { get; set; }
[Parameter] public required RenderFragment ChildContent { get; set; }
[Parameter] public required string ButtonText { get; set; }
[Parameter] public required EventCallback ButtonClickCallback { get; set; }
}
13 changes: 13 additions & 0 deletions WardrobeManager.Presentation/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@

@code {
private ErrorBoundary? errorBoundary;


// This runs every time any page is (fully) reloaded (in the browser)
protected override async Task OnInitializedAsync()
{
var res = await _apiService.DoesAdminUserExist();
if (res is false)
{
_navManager.NavigateTo("/onboarding");
}

await base.OnInitializedAsync();
}

protected override void OnParametersSet()
{
Expand Down
105 changes: 105 additions & 0 deletions WardrobeManager.Presentation/Pages/Public/Onboarding.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
@page "/onboarding"

@using System.ComponentModel.DataAnnotations
@using WardrobeManager.Presentation.Components.Onboarding
@using WardrobeManager.Presentation.Components.FormItems
@namespace WardrobeManager.Presentation.Pages.Public

<PageTitle>Onboarding - WardrobeManager</PageTitle>


<div class="h-full flex flex-col items-center bg-primary-content gap-10">
<h3 class="heading-text">Welcome to WardrobeManager!</h3>

<div class="grow flex ">
@switch (currentSectionIndex)
{
case 0:
{
<OnboardingSection Title="@($"{ProjectConstants.ProjectName} is an all-in-one solution to clothing management.")"
ButtonText="Next"
ButtonClickCallback="GoToNextSection">
<img src="@ProjectConstants.HomeBackgroundImage" class="h-96 object-contain rounded-2xl"/>
</OnboardingSection>
break;
}
case 1:
{
<OnboardingSection Title="@($"Enter the credentials for the administrator account")"
ButtonText="Next"
ButtonClickCallback="CreateAdminUser">
<div>
<LabelAndElement Label="Username" Orientation="vertical">
<InputText @bind-Value="email" required class="bg-base-200 text-base-content p-3 rounded-xl" placeholder="@("[email protected]")" type="email"/>
</LabelAndElement>
<LabelAndElement Label="Password" Orientation="vertical">
<InputText @bind-Value="password" required class="bg-base-200 text-base-content p-3 rounded-xl" placeholder="............" type="password"/>
</LabelAndElement>


</div>
</OnboardingSection>
break;
}
case 2:
{
<OnboardingSection Title="@($"Enjoy using {ProjectConstants.ProjectName}!")"
ButtonText="Go To Dashboard"
ButtonClickCallback="@(() => _navManager.NavigateTo("/dashboard"))">
<p class="subtitle-text">
If you encounter any issues, please open an issue on the <a href="@ProjectConstants.ProjectGitRepo" class="text-accent">GitHub repo</a>
</p>
</OnboardingSection>
break;
}
}

</div>

</div>

@code {
private int currentSectionIndex = 0;

private string email = string.Empty;
private string password = string.Empty;

protected override async Task OnInitializedAsync()
{
var exists = await _apiService.DoesAdminUserExist();
if (exists)
{
_navManager.NavigateTo("/login");
}

await base.OnInitializedAsync();
}

public void GoToNextSection()
{
currentSectionIndex++;
StateHasChanged();
}

public async Task CreateAdminUser()
{
if (email == string.Empty || password == string.Empty)
{
_notificationService.AddNotification("You must specify a username and password!", NotificationType.Warning);
}

var credentials = new AdminUserCredentials
{
email = email, password = password
};
var res = await _apiService.CreateAdminUserIfMissing(credentials);
_notificationService.AddNotification(res.Item2);

// If added the admin user was sucessful
if (res.Item1 is true)
{
GoToNextSection();
}
}

}
Loading

0 comments on commit e09476c

Please sign in to comment.