mirror of
https://github.com/claunia/marechai.git
synced 2026-07-08 17:57:08 +00:00
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.
104 lines
3.9 KiB
C#
104 lines
3.9 KiB
C#
/*******************************************************************************
|
||
// 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
|
||
*******************************************************************************/
|
||
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using Marechai.Database.Models;
|
||
using Microsoft.EntityFrameworkCore;
|
||
|
||
namespace Marechai.Server.Services;
|
||
|
||
/// <summary>
|
||
/// Generates fresh invitation codes in the canonical <c>XXXX-YYYY</c> format using a 31-character
|
||
/// unambiguous alphabet (no <c>0</c>/<c>1</c>/<c>I</c>/<c>L</c>/<c>O</c>) and the OS cryptographic RNG.
|
||
/// A code carries ~40 bits of entropy (8 chars × log2(31)); bias-free even on a non-power-of-two alphabet
|
||
/// because <see cref="RandomNumberGenerator.GetInt32(int, int)" /> rejects-and-retries internally.
|
||
/// </summary>
|
||
public sealed class InvitationCodeGenerator
|
||
{
|
||
/// <summary>
|
||
/// Unambiguous 31-character alphabet: A–Z minus I/L/O (23 chars) plus 2–9 (8 chars). 0 and 1 are excluded
|
||
/// because they look like O and I when handwritten or printed in some fonts.
|
||
/// </summary>
|
||
const string Alphabet = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
||
|
||
public string Generate()
|
||
{
|
||
var sb = new StringBuilder(9);
|
||
|
||
for(int i = 0; i < 8; i++)
|
||
{
|
||
if(i == 4) sb.Append('-');
|
||
sb.Append(Alphabet[RandomNumberGenerator.GetInt32(0, Alphabet.Length)]);
|
||
}
|
||
|
||
return sb.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Generates and persists N unique invitation codes for a given owner, with the same collision-retry logic
|
||
/// used in single-code generation. Each code is saved individually (not batched) to match the existing pattern.
|
||
/// Returns the list of created codes.
|
||
/// </summary>
|
||
public async Task<List<InvitationCode>> GenerateForOwnerAsync(MarechaiContext context, string ownerId, int count)
|
||
{
|
||
var created = new List<InvitationCode>();
|
||
|
||
for(int codeIndex = 0; codeIndex < count; codeIndex++)
|
||
{
|
||
for(int attempt = 0; attempt < 5; attempt++)
|
||
{
|
||
string code = Generate();
|
||
|
||
var entity = new InvitationCode
|
||
{
|
||
Code = code,
|
||
CreatedById = ownerId,
|
||
CreatedOn = DateTime.UtcNow,
|
||
RowVersion = Guid.NewGuid()
|
||
};
|
||
|
||
context.InvitationCodes.Add(entity);
|
||
|
||
try
|
||
{
|
||
await context.SaveChangesWithUserAsync(ownerId);
|
||
created.Add(entity);
|
||
break;
|
||
}
|
||
catch(DbUpdateException)
|
||
{
|
||
context.Entry(entity).State = EntityState.Detached;
|
||
}
|
||
}
|
||
}
|
||
|
||
return created;
|
||
}
|
||
}
|