diff --git a/.gitignore b/.gitignore
index 4e5b483..ebec2fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,4 +55,8 @@ inventory/.copilot_instructions.md
# ============================
publish/
+.dpkeys/
+**/.dpkeys/
+
+
appsettings.Development.json
diff --git a/CulinaryCommandApp/Components/Pages/Users/Create.razor b/CulinaryCommandApp/Components/Pages/Users/Create.razor
index 8a9db9b..53b557c 100644
--- a/CulinaryCommandApp/Components/Pages/Users/Create.razor
+++ b/CulinaryCommandApp/Components/Pages/Users/Create.razor
@@ -1,20 +1,149 @@
@page "/users/create"
-@using CulinaryCommand.Data.Entities
+@rendermode InteractiveServer
+
+@using CulinaryCommand.Services.UserContextSpace
@inject IUserService UserService
+@inject IUserContextService UserCtx
+@inject LocationState LocationState
@inject NavigationManager Nav
-@rendermode InteractiveServer
-
Create User
+Invite User
+
+@if (!_hydrated)
+{
+ Loading...
+}
+else if (!_allowed)
+{
+ You do not have permission to invite users.
+}
+else
+{
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
@code {
- private User model = new();
+ private bool _hydrated;
+ private bool _allowed;
+ private bool _saving;
+
+ private int _companyId;
+ private int _createdByUserId;
+
+ private InviteUserVm _model = new()
+ {
+ Role = "Employee",
+ LocationIds = new List()
+ };
+
+ protected override async Task OnInitializedAsync()
+ {
+ var ctx = await UserCtx.GetAsync();
+
+ if (ctx.IsAuthenticated != true)
+ {
+ Nav.NavigateTo("/login", true);
+ return;
+ }
+
+ if (ctx.User is null)
+ {
+ Nav.NavigateTo("/no-access", true);
+ return;
+ }
+
+ _allowed =
+ string.Equals(ctx.User.Role, "Admin", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(ctx.User.Role, "Manager", StringComparison.OrdinalIgnoreCase);
+
+ _companyId = ctx.User.CompanyId ?? 0;
+ _createdByUserId = ctx.User.Id;
+
+ // default to current location if you want
+ var current = LocationState.CurrentLocation;
+ if (current != null && !_model.LocationIds.Contains(current.Id))
+ _model.LocationIds.Add(current.Id);
+
+ _hydrated = true;
+ }
+
+ private Task OnLocationsChanged(List ids)
+ {
+ _model.LocationIds = ids ?? new List();
+ return Task.CompletedTask;
+ }
+
+ private async Task Save()
+ {
+ if (_saving) return;
+ _saving = true;
+ try
+ {
+ await UserService.InviteUserAsync(
+ _model.FirstName,
+ _model.LastName,
+ _model.Email,
+ _model.Role,
+ _companyId,
+ _createdByUserId,
+ _model.LocationIds
+ );
+
+ Nav.NavigateTo("/users");
+ }
+ finally
+ {
+ _saving = false;
+ }
+ }
+
+ private void Cancel() => Nav.NavigateTo("/users");
- private async Task Save(User u)
+ private class InviteUserVm
{
- await UserService.CreateUserAsync(u);
- await UserService.AssignLocationsAsync(u.Id, u.UserLocations.Select(x => x.LocationId).ToList());
- Nav.NavigateTo("/users");
+ public string FirstName { get; set; } = "";
+ public string LastName { get; set; } = "";
+ public string Email { get; set; } = "";
+ public string Role { get; set; } = "Employee";
+ public List LocationIds { get; set; } = new();
}
}
diff --git a/CulinaryCommandApp/Services/CognitoProvisioningService.cs b/CulinaryCommandApp/Services/CognitoProvisioningService.cs
index 817b781..e000044 100644
--- a/CulinaryCommandApp/Services/CognitoProvisioningService.cs
+++ b/CulinaryCommandApp/Services/CognitoProvisioningService.cs
@@ -50,7 +50,7 @@ await _cognito.AdminCreateUserAsync(new AdminCreateUserRequest
}
catch (Exception ex)
{
- throw new InvalidOperationException("Failed to create Cognito user.", ex);
+ throw new InvalidOperationException($"Failed to create Cognito user. {ex.GetType().Name}: {ex.Message}", ex);
}
try
diff --git a/CulinaryCommandApp/Services/UserService.cs b/CulinaryCommandApp/Services/UserService.cs
index cd031cf..e233677 100644
--- a/CulinaryCommandApp/Services/UserService.cs
+++ b/CulinaryCommandApp/Services/UserService.cs
@@ -29,6 +29,7 @@ public interface IUserService
Task SendInviteEmailAsync(User user);
Task GetUserByInviteTokenAsync(string token);
Task ActivateUserAsync(string token, string password);
+ Task InviteUserAsync(string firstName, string lastName, string email, string role, int companyId, int createdByUserId, List locationIds);
}
public class UserService : IUserService
@@ -50,7 +51,7 @@ public async Task CreateUserAsync(User user)
{
// check that the user with that email DOESN'T exist
if (await _context.Users.AnyAsync(u => u.Email == user.Email))
- { throw new Exception("Email already exists.");}
+ { throw new Exception("Email already exists."); }
user.Password = HashPassword(user.Password!);
user.CreatedAt = DateTime.UtcNow;
@@ -447,7 +448,7 @@ public async Task SendInviteEmailAsync(User user)
if (string.IsNullOrWhiteSpace(user.InviteToken))
throw new InvalidOperationException("User does not have an invite token.");
- string link = $"https://yourdomain.com/account/setup?token={user.InviteToken}";
+ string link = $"https://culinary-command.com/account/setup?token={user.InviteToken}";
string subject = "Your CulinaryCommand Account Invitation";
string body = $@"
@@ -474,22 +475,86 @@ public async Task SendInviteEmailAsync(User user)
public async Task ActivateUserAsync(string token, string password)
{
var user = await GetUserByInviteTokenAsync(token);
+ if (user == null) return false;
- if (user == null)
- return false;
+ // Create the user in Cognito (so they can log in)
+ await _cognito.CreateUserWithPasswordAsync(
+ user.Email!,
+ user.Name ?? user.Email!,
+ password
+ );
- user.Password = HashPassword(password);
- //user.CreatedAt = DateTime.UtcNow;
+ // Mark active in DB (your existing logic)
+ user.Password = HashPassword(password);
user.UpdatedAt = DateTime.UtcNow;
user.IsActive = true;
user.EmailConfirmed = true;
user.InviteToken = null;
user.InviteTokenExpires = null;
- user.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
return true;
}
+ public async Task InviteUserAsync(
+ string firstName,
+ string lastName,
+ string email,
+ string role,
+ int companyId,
+ int createdByUserId,
+ List locationIds)
+ {
+ if (string.IsNullOrWhiteSpace(email)) throw new InvalidOperationException("Email is required.");
+ if (locationIds == null || locationIds.Count == 0) throw new InvalidOperationException("At least one location must be selected.");
+
+ email = email.Trim().ToLowerInvariant();
+
+ // create invited user for ONE location (your existing method)
+ var primaryLocId = locationIds[0];
+
+ var user = await CreateInvitedUserForLocationAsync(new CreateUserForLocationRequest
+ {
+ FirstName = firstName?.Trim() ?? "",
+ LastName = lastName?.Trim() ?? "",
+ Email = email,
+ Role = role,
+ LocationId = primaryLocId
+ }, companyId, createdByUserId);
+
+ // now assign any additional locations (since CreateInvitedUserForLocationAsync only attaches one)
+ var extraLocs = locationIds.Skip(1).ToList();
+ if (extraLocs.Count > 0)
+ {
+ // add userlocations for extras
+ foreach (var locId in extraLocs)
+ {
+ _context.UserLocations.Add(new UserLocation
+ {
+ UserId = user.Id,
+ LocationId = locId
+ });
+
+ // if manager, add manager mapping too
+ if (string.Equals(role, "Manager", StringComparison.OrdinalIgnoreCase))
+ {
+ _context.ManagerLocations.Add(new ManagerLocation
+ {
+ UserId = user.Id,
+ LocationId = locId
+ });
+ }
+ }
+
+ await _context.SaveChangesAsync();
+ }
+
+ // send invite email
+ await SendInviteEmailAsync(user);
+
+ return user;
+ }
+
+
}
}
\ No newline at end of file