Files
marechai/Marechai/Pages/Admin/Users.razor
Natalia Portillo cb781ecd57 feat: Add automatic invitation code generation on email confirmation
Implement a new feature where users receive 5 invitation codes when they confirm their email.

Backend changes:
- Extend InvitationCodeGenerator with GenerateForOwnerAsync() for batch code creation
- Mint 5 codes on email confirmation (not registration) in AuthController.ConfirmEmailAsync
- Add GET /invitation-codes/mine (self-service) endpoint for users to view their own codes
- Add POST /users/{id}/invitation-codes (admin-only) endpoint to grant additional codes
- Create MyInvitationCodeDto and GrantInvitationCodesRequest DTOs

Frontend changes:
- Add "My Invitation Codes" tab to Profile.razor with list of user's codes
  - Unused codes rendered normally with copy-to-clipboard button
  - Used codes shown with strikethrough (no user identity revealed)
- Add "Give Invitation Codes" action to admin Users page
  - Dialog prompts for count (1-50) of codes to grant to selected user
  - Success/error messages displayed after granting
- Extend InvitationCodesService.GetMyAsync() and UsersService.GrantInvitationCodesAsync()
- Regenerate Kiota API client from updated OpenAPI schema

No database schema changes required - existing InvitationCode fields accommodate both
registration-issued and admin-granted codes.
2026-06-19 22:28:54 +01:00

116 lines
6.6 KiB
Plaintext

@{
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2026 Natalia Portillo
*******************************************************************************/
}
@page "/admin/users"
@attribute [Authorize(Roles = "UberAdmin")]
@using Marechai.ApiClient.Models
@using Marechai.Services
@inject UsersService UsersService
@inject InvitationCodesService InvitationCodesService
@inject IStringLocalizer<UsersService> L
@inject IDialogService DialogService
@inject ISnackbar Snackbar
<MudPaper Class="pa-6 mb-6" Elevation="0" Style="background: linear-gradient(135deg, #7e57c2 0%, #42a5f5 100%); border-radius: 16px;">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Filled.AdminPanelSettings" Size="Size.Large" Style="color: white;"/>
<MudText Typo="Typo.h4" Style="color: white;">@L["User Management"]</MudText>
</MudStack>
<MudText Typo="Typo.subtitle1" Style="color: rgba(255,255,255,0.85);" Class="mt-2">@L["Manage users, passwords, and role assignments."]</MudText>
</MudPaper>
@if(!string.IsNullOrWhiteSpace(_errorMessage))
{
<MudAlert Severity="Severity.Error" Class="mb-4" ShowCloseIcon="true" CloseIconClicked="() => _errorMessage = null">@_errorMessage</MudAlert>
}
@if(!string.IsNullOrWhiteSpace(_successMessage))
{
<MudAlert Severity="Severity.Success" Class="mb-4" ShowCloseIcon="true" CloseIconClicked="() => _successMessage = null">@_successMessage</MudAlert>
}
<MudPaper Elevation="2" Class="rounded-lg">
<MudToolBar Dense="true">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.PersonAdd" OnClick="OpenAddUserDialog">@L["Add User"]</MudButton>
<MudSpacer/>
@if(_selectedUsers.Count > 0)
{
<MudChip T="string" Size="Size.Small" Color="Color.Info" Variant="Variant.Filled">@string.Format(L["{0} selected"], _selectedUsers.Count)</MudChip>
<MudButton Variant="Variant.Outlined" Color="Color.Error" StartIcon="@Icons.Material.Filled.DeleteSweep" Size="Size.Small" OnClick="ConfirmBulkDeleteUsers" Class="ml-2">@L["Delete"]</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Security" Size="Size.Small" OnClick="OpenBulkRoleDialog" Class="ml-1">@L["Roles"]</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Lock" Size="Size.Small" OnClick="OpenBulkLockoutDialog" Class="ml-1">@L["Lockout"]</MudButton>
}
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="@(async () => await _dataGrid.ReloadServerData())"/>
</MudToolBar>
<MudDataGrid @ref="_dataGrid" T="UserDto" ServerData="ServerReload" RowsPerPage="25" Dense="true" Hover="true" Striped="true" Filterable="true" MultiSelection="true" @bind-SelectedItems="_selectedUsers">
<Columns>
<SelectColumn T="UserDto"/>
<PropertyColumn Property="x => x.Email" Title="@L["Email"]"/>
<PropertyColumn Property="x => x.UserName" Title="@L["Username"]"/>
<PropertyColumn Property="x => x.PhoneNumber" Title="@L["Phone"]"/>
<TemplateColumn Title="@L["Roles"]" PropertyName="Roles" Sortable="false">
<CellTemplate>
<MudStack Row="true" Spacing="1">
@if(context.Item.Roles is { Count: > 0 })
{
@foreach(string role in context.Item.Roles)
{
<MudChip T="string" Size="Size.Small" Color="Color.Primary" Variant="Variant.Outlined">@role</MudChip>
}
}
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="@L["Actions"]" Sortable="false" Filterable="false">
<CellTemplate>
<MudStack Row="true" Spacing="1">
<MudTooltip Text="@L["Edit"]"><MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" OnClick="() => OpenEditUserDialog(context.Item)"/></MudTooltip>
<MudTooltip Text="@L["Change Password"]"><MudIconButton Icon="@Icons.Material.Filled.Lock" Size="Size.Small" OnClick="() => OpenChangePasswordDialog(context.Item)"/></MudTooltip>
<MudTooltip Text="@L["Manage Roles"]"><MudIconButton Icon="@Icons.Material.Filled.Security" Size="Size.Small" OnClick="() => OpenManageRolesDialog(context.Item)"/></MudTooltip>
<MudTooltip Text="@L["Give Invitation Codes"]"><MudIconButton Icon="@Icons.Material.Filled.ConfirmationNumber" Size="Size.Small" OnClick="() => OpenGiveInvitationCodesDialog(context.Item)"/></MudTooltip>
@if(context.Item.TwoFactorEnabled == true)
{
<MudTooltip Text="@L["Disable 2FA"]"><MudIconButton Icon="@Icons.Material.Filled.LockOpen" Size="Size.Small" Color="Color.Warning" OnClick="() => ConfirmDisableTwoFactor(context.Item)"/></MudTooltip>
}
<MudTooltip Text="@L["Delete"]"><MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error" OnClick="() => ConfirmDeleteUser(context.Item)"/></MudTooltip>
</MudStack>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="UserDto" PageSizeOptions="new[] { 10, 25, 50, 100 }"/>
</PagerContent>
</MudDataGrid>
</MudPaper>