Files
marechai/Marechai.Server/Services/InvitationCodeGenerator.cs
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

104 lines
3.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*******************************************************************************
// 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: AZ minus I/L/O (23 chars) plus 29 (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;
}
}