Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/backend
diff options
context:
space:
mode:
authorLeitwolf112026-09-07 21:30:40 +0300
committerLeitwolf112026-09-07 21:30:40 +0300
commitc0cd5f4699e44899956c19176ca47ede27a21a8f (patch)
treedc10ba78fde4c4dfaa91f4d43ffaee7bcb6c78b7 /backend
parente77f4585d3a1be272fe83d5a06b1d2a32fa8f7b5 (diff)
downloadwebsite-c0cd5f4699e44899956c19176ca47ede27a21a8f.tar.gz
Separate ViGet from Progmasoft Account infrastructure
Move the Progmasoft mail operations baseline to its owning Progmaweb repository, remove the retired Astro account UI and duplicate identity backend, keep only the ViGet health boundary, add canonical sitemap signals, strengthen dependency CI, and adopt Patent Grant 1.1 across project-owned files.
Diffstat (limited to 'backend')
-rw-r--r--backend/Auth/AuthCodeOptions.cs11
-rw-r--r--backend/Auth/AuthCodeService.cs74
-rw-r--r--backend/Auth/AuthEndpoints.cs373
-rw-r--r--backend/Auth/PublisherNamePolicy.cs56
-rw-r--r--backend/Data/ApplicationUser.cs13
-rw-r--r--backend/Data/AuthChallenge.cs15
-rw-r--r--backend/Data/Migrations/20260726172221_InitialRegistryIdentity.Designer.cs365
-rw-r--r--backend/Data/Migrations/20260726172221_InitialRegistryIdentity.cs327
-rw-r--r--backend/Data/Migrations/20260728182403_CascadeAccountOwnedRecords.Designer.cs383
-rw-r--r--backend/Data/Migrations/20260728182403_CascadeAccountOwnedRecords.cs68
-rw-r--r--backend/Data/Migrations/20260820173435_AddPublisherNames.Designer.cs393
-rw-r--r--backend/Data/Migrations/20260820173435_AddPublisherNames.cs57
-rw-r--r--backend/Data/Migrations/RegistryDbContextModelSnapshot.cs393
-rw-r--r--backend/Data/RegistryDbContext.cs58
-rw-r--r--backend/Data/RegistryToken.cs17
-rw-r--r--backend/Email/EmailOptions.cs12
-rw-r--r--backend/Email/RegistryEmailSender.cs37
-rw-r--r--backend/Program.cs121
-rw-r--r--backend/Properties/AssemblyInfo.cs2
-rw-r--r--backend/Tokens/TokenEndpoints.cs116
-rw-r--r--backend/XSharp.Web.Api.csproj11
-rw-r--r--backend/appsettings.json25
22 files changed, 10 insertions, 2917 deletions
diff --git a/backend/Auth/AuthCodeOptions.cs b/backend/Auth/AuthCodeOptions.cs
deleted file mode 100644
index b4fa48d..0000000
--- a/backend/Auth/AuthCodeOptions.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.0
-
-namespace XSharp.Web.Api.Auth;
-
-internal sealed class AuthCodeOptions
-{
- public required string Pepper { get; init; }
- public int LifetimeMinutes { get; init; } = 15;
- public int MaximumAttempts { get; init; } = 6;
-}
diff --git a/backend/Auth/AuthCodeService.cs b/backend/Auth/AuthCodeService.cs
deleted file mode 100644
index 64135fd..0000000
--- a/backend/Auth/AuthCodeService.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.0
-
-using System.Security.Cryptography;
-using System.Text;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Options;
-using XSharp.Web.Api.Data;
-
-namespace XSharp.Web.Api.Auth;
-
-internal sealed class AuthCodeService(RegistryDbContext database, IOptions<AuthCodeOptions> options)
-{
- private const string Alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
- private readonly AuthCodeOptions configuration = options.Value;
-
- public async Task<string> CreateAsync(Guid userId, string purpose, CancellationToken cancellationToken)
- {
- if (string.IsNullOrWhiteSpace(configuration.Pepper))
- {
- throw new InvalidOperationException("AuthCodes:Pepper is required.");
- }
-
- await database.AuthChallenges
- .Where(item => item.UserId == userId && item.Purpose == purpose && item.ConsumedAt == null)
- .ExecuteUpdateAsync(setters => setters.SetProperty(item => item.ConsumedAt, DateTimeOffset.UtcNow),
- cancellationToken);
-
- string code = string.Create(8, 0, static (buffer, _) =>
- {
- for (int index = 0; index < buffer.Length; ++index)
- {
- buffer[index] = Alphabet[RandomNumberGenerator.GetInt32(Alphabet.Length)];
- }
- });
- database.AuthChallenges.Add(new AuthChallenge
- {
- UserId = userId,
- Purpose = purpose,
- CodeDigest = Digest(code, configuration.Pepper),
- ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(configuration.LifetimeMinutes),
- });
- await database.SaveChangesAsync(cancellationToken);
- return code;
- }
-
- public async Task<bool> ConsumeAsync(Guid userId, string purpose, string code, CancellationToken cancellationToken)
- {
- AuthChallenge? challenge = await database.AuthChallenges
- .Where(item => item.UserId == userId && item.Purpose == purpose && item.ConsumedAt == null)
- .OrderByDescending(item => item.ExpiresAt)
- .FirstOrDefaultAsync(cancellationToken);
- if (challenge is null || challenge.ExpiresAt <= DateTimeOffset.UtcNow
- || challenge.FailedAttempts >= configuration.MaximumAttempts)
- {
- return false;
- }
-
- byte[] supplied = Digest(code.Trim().ToUpperInvariant(), configuration.Pepper);
- if (!CryptographicOperations.FixedTimeEquals(challenge.CodeDigest, supplied))
- {
- ++challenge.FailedAttempts;
- await database.SaveChangesAsync(cancellationToken);
- return false;
- }
-
- challenge.ConsumedAt = DateTimeOffset.UtcNow;
- await database.SaveChangesAsync(cancellationToken);
- return true;
- }
-
- private static byte[] Digest(string code, string pepper) =>
- SHA256.HashData(Encoding.UTF8.GetBytes($"{pepper}:{code}"));
-}
diff --git a/backend/Auth/AuthEndpoints.cs b/backend/Auth/AuthEndpoints.cs
deleted file mode 100644
index d866a48..0000000
--- a/backend/Auth/AuthEndpoints.cs
+++ /dev/null
@@ -1,373 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.0
-
-using System.Security.Claims;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Identity;
-using Microsoft.AspNetCore.RateLimiting;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Storage;
-using Npgsql;
-using XSharp.Web.Api.Data;
-using XSharp.Web.Api.Email;
-
-namespace XSharp.Web.Api.Auth;
-
-internal static class AuthEndpoints
-{
- private const string DashboardUrl = "https://account.progmasoft.com/";
- private const string LoginUrl = "https://account.progmasoft.com/login";
- private const string RegisterUrl = "https://account.progmasoft.com/register";
-
- public static void MapAuthEndpoints(this WebApplication app)
- {
- RouteGroupBuilder auth = app.MapGroup("/api/v1/auth").RequireRateLimiting("auth");
- auth.MapPost("/register", RegisterAsync);
- auth.MapPost("/verify-email", VerifyEmailAsync);
- auth.MapPost("/verify-email/resend", ResendVerificationAsync);
- auth.MapPost("/login", LoginAsync);
- auth.MapPost("/logout", LogoutAsync).RequireAuthorization();
- auth.MapDelete("/account", DeleteAccountAsync).RequireAuthorization();
- auth.MapGet("/me", MeAsync).RequireAuthorization();
- auth.MapGet("/providers", ProvidersAsync);
- auth.MapGet("/google", BeginGoogleAsync);
- auth.MapGet("/google/complete", CompleteGoogleAsync);
- auth.MapPost("/recovery/start", StartRecoveryAsync);
- auth.MapPost("/recovery/complete", CompleteRecoveryAsync);
- }
-
- private static async Task<IResult> RegisterAsync(
- RegisterRequest request,
- UserManager<ApplicationUser> users,
- AuthCodeService codes,
- RegistryEmailSender emailSender,
- CancellationToken cancellationToken)
- {
- if (!TryGetCredentials(request.Email, request.Password, out string email, out string password))
- {
- return InvalidRequest();
- }
- PublisherNameValidation publisherName = PublisherNamePolicy.Validate(request.PublisherName);
- if (!publisherName.IsValid)
- {
- return InvalidPublisherName(publisherName.Error!);
- }
- if (await PublisherNameExistsAsync(users, publisherName.NormalizedValue, cancellationToken))
- {
- return PublisherNameUnavailable();
- }
-
- // Availability is checked before sending mail for fast feedback, but the name is claimed only after the email code
- // succeeds. Unverified accounts therefore cannot squat publisher coordinates.
- ApplicationUser user = new() { UserName = email, Email = email };
- IdentityResult result = await users.CreateAsync(user, password);
- if (!result.Succeeded)
- {
- return IdentityErrors(result);
- }
-
- string code = await codes.CreateAsync(user.Id, "signup", cancellationToken);
- await emailSender.SendCodeAsync(email, code, "signup");
- return Results.Accepted(value: new { verificationRequired = true });
- }
-
- private static async Task<IResult> VerifyEmailAsync(
- VerificationRequest request,
- UserManager<ApplicationUser> users,
- AuthCodeService codes,
- SignInManager<ApplicationUser> signIn,
- RegistryDbContext database,
- CancellationToken cancellationToken)
- {
- if (!TryNormalizeEmail(request.Email, out string email) || string.IsNullOrWhiteSpace(request.Code))
- {
- return InvalidRequest();
- }
- ApplicationUser? user = await users.FindByEmailAsync(email);
- if (user is null)
- {
- return Results.BadRequest(new ApiError("invalid_code", "The verification code is invalid or expired."));
- }
-
- await using IDbContextTransaction transaction = await database.Database.BeginTransactionAsync(cancellationToken);
- PublisherNameValidation publisherName = default;
- if (user.PublisherName is null)
- {
- publisherName = PublisherNamePolicy.Validate(request.PublisherName);
- if (!publisherName.IsValid)
- {
- return InvalidPublisherName(publisherName.Error!);
- }
- if (await PublisherNameExistsAsync(users, publisherName.NormalizedValue, cancellationToken))
- {
- return PublisherNameUnavailable();
- }
- }
- if (!await codes.ConsumeAsync(user.Id, "signup", request.Code, cancellationToken))
- {
- return Results.BadRequest(new ApiError("invalid_code", "The verification code is invalid or expired."));
- }
-
- if (user.PublisherName is null)
- {
- user.PublisherName = publisherName.Value;
- user.NormalizedPublisherName = publisherName.NormalizedValue;
- }
- user.EmailConfirmed = true;
- IdentityResult result;
- try
- {
- result = await users.UpdateAsync(user);
- }
- catch (DbUpdateException exception) when (IsPublisherNameConflict(exception))
- {
- return PublisherNameUnavailable();
- }
- if (!result.Succeeded) return IdentityErrors(result);
- await transaction.CommitAsync(cancellationToken);
- await signIn.SignInAsync(user, isPersistent: true);
- return Results.Ok(new { verified = true });
- }
-
- private static async Task<IResult> ResendVerificationAsync(
- EmailRequest request,
- UserManager<ApplicationUser> users,
- AuthCodeService codes,
- RegistryEmailSender emailSender,
- CancellationToken cancellationToken)
- {
- if (TryNormalizeEmail(request.Email, out string email))
- {
- ApplicationUser? user = await users.FindByEmailAsync(email);
- if (user is not null && !user.EmailConfirmed)
- {
- string code = await codes.CreateAsync(user.Id, "signup", cancellationToken);
- await emailSender.SendCodeAsync(email, code, "signup");
- }
- }
- return Results.Accepted(value: new { codeSent = true });
- }
-
- private static async Task<IResult> LoginAsync(
- Credentials request,
- UserManager<ApplicationUser> users,
- SignInManager<ApplicationUser> signIn)
- {
- if (!TryGetCredentials(request.Email, request.Password, out string email, out string password))
- {
- return InvalidCredentials();
- }
- ApplicationUser? user = await users.FindByEmailAsync(email);
- if (user is null)
- {
- return InvalidCredentials();
- }
- SignInResult result = await signIn.PasswordSignInAsync(user, password, true, true);
- if (result.IsNotAllowed)
- {
- return Results.Json(new ApiError("email_unverified", "Verify your email before signing in."),
- statusCode: StatusCodes.Status403Forbidden);
- }
- return result.Succeeded ? Results.Ok(new { authenticated = true }) : InvalidCredentials();
- }
-
- private static async Task<IResult> LogoutAsync(SignInManager<ApplicationUser> signIn)
- {
- await signIn.SignOutAsync();
- return Results.NoContent();
- }
-
- private static async Task<IResult> MeAsync(
- ClaimsPrincipal principal,
- UserManager<ApplicationUser> users)
- {
- ApplicationUser? user = await users.GetUserAsync(principal);
- if (user is null) return Results.Unauthorized();
- return Results.Ok(new
- {
- id = user.Id,
- email = user.Email,
- publisherName = user.PublisherName,
- displayName = user.PublisherName ?? user.Email,
- emailVerified = user.EmailConfirmed,
- });
- }
-
- private static async Task<IResult> ProvidersAsync(IAuthenticationSchemeProvider schemes) =>
- Results.Ok(new { google = await schemes.GetSchemeAsync("Google") is not null });
-
- private static async Task<IResult> BeginGoogleAsync(
- IAuthenticationSchemeProvider schemes,
- SignInManager<ApplicationUser> signIn)
- {
- if (await schemes.GetSchemeAsync("Google") is null)
- {
- return Results.Problem("Google OAuth is not configured.", statusCode: StatusCodes.Status503ServiceUnavailable);
- }
- AuthenticationProperties properties = signIn.ConfigureExternalAuthenticationProperties(
- "Google", "/api/v1/auth/google/complete");
- return Results.Challenge(properties, ["Google"]);
- }
-
- private static async Task<IResult> CompleteGoogleAsync(
- SignInManager<ApplicationUser> signIn,
- UserManager<ApplicationUser> users,
- AuthCodeService codes,
- RegistryEmailSender emailSender,
- CancellationToken cancellationToken)
- {
- ExternalLoginInfo? info = await signIn.GetExternalLoginInfoAsync();
- if (info is null) return Results.Redirect($"{LoginUrl}?error=oauth");
-
- if (!TryNormalizeEmail(info.Principal.FindFirstValue(ClaimTypes.Email), out string email))
- {
- return Results.Redirect($"{LoginUrl}?error=missing_email");
- }
- ApplicationUser? user = await users.FindByEmailAsync(email);
- if (user is null)
- {
- user = new ApplicationUser { UserName = email, Email = email };
- IdentityResult created = await users.CreateAsync(user);
- if (!created.Succeeded) return Results.Redirect($"{LoginUrl}?error=account");
- }
-
- ApplicationUser? loginOwner = await users.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
- if (loginOwner is null)
- {
- IdentityResult loginAdded = await users.AddLoginAsync(user, info);
- if (!loginAdded.Succeeded) return Results.Redirect($"{LoginUrl}?error=oauth");
- }
- else if (loginOwner.Id != user.Id)
- {
- return Results.Redirect($"{LoginUrl}?error=oauth");
- }
-
- string code = await codes.CreateAsync(user.Id, "signup", cancellationToken);
- await emailSender.SendCodeAsync(email, code, "signup");
- string destination = $"{RegisterUrl}?verify=google"
- + $"&email={Uri.EscapeDataString(email)}";
- return Results.Redirect(destination);
- }
-
- private static async Task<IResult> DeleteAccountAsync(
- [Microsoft.AspNetCore.Mvc.FromBody] DeleteAccountRequest request,
- ClaimsPrincipal principal,
- UserManager<ApplicationUser> users,
- SignInManager<ApplicationUser> signIn)
- {
- ApplicationUser? user = await users.GetUserAsync(principal);
- if (user?.Email is null)
- {
- return Results.Unauthorized();
- }
- string confirmation = request.Confirmation?.Trim() ?? string.Empty;
- if (!string.Equals(confirmation, user.Email, StringComparison.OrdinalIgnoreCase))
- {
- return Results.BadRequest(new ApiError(
- "confirmation_mismatch",
- "Enter the account email address to confirm deletion."));
- }
-
- IdentityResult result = await users.DeleteAsync(user);
- if (!result.Succeeded) return IdentityErrors(result);
- await signIn.SignOutAsync();
- return Results.NoContent();
- }
-
- private static async Task<IResult> StartRecoveryAsync(
- EmailRequest request,
- UserManager<ApplicationUser> users,
- AuthCodeService codes,
- RegistryEmailSender emailSender,
- CancellationToken cancellationToken)
- {
- if (!TryNormalizeEmail(request.Email, out string email))
- {
- return Results.Accepted(value: new { codeSent = true });
- }
- ApplicationUser? user = await users.FindByEmailAsync(email);
- if (user is not null && user.EmailConfirmed)
- {
- string code = await codes.CreateAsync(user.Id, "recovery", cancellationToken);
- await emailSender.SendCodeAsync(email, code, "recovery");
- }
- return Results.Accepted(value: new { codeSent = true });
- }
-
- private static async Task<IResult> CompleteRecoveryAsync(
- RecoveryRequest request,
- UserManager<ApplicationUser> users,
- AuthCodeService codes,
- SignInManager<ApplicationUser> signIn,
- CancellationToken cancellationToken)
- {
- if (!TryNormalizeEmail(request.Email, out string email)
- || string.IsNullOrWhiteSpace(request.Code)
- || string.IsNullOrWhiteSpace(request.Password))
- {
- return InvalidRequest();
- }
- ApplicationUser? user = await users.FindByEmailAsync(email);
- if (user is null || !await codes.ConsumeAsync(user.Id, "recovery", request.Code, cancellationToken))
- {
- return Results.BadRequest(new ApiError("invalid_code", "The recovery code is invalid or expired."));
- }
- string resetToken = await users.GeneratePasswordResetTokenAsync(user);
- IdentityResult result = await users.ResetPasswordAsync(user, resetToken, request.Password);
- if (!result.Succeeded) return IdentityErrors(result);
- await signIn.SignInAsync(user, isPersistent: true);
- return Results.Ok(new { recovered = true });
- }
-
- private static bool TryGetCredentials(
- string? requestedEmail,
- string? requestedPassword,
- out string email,
- out string password)
- {
- password = requestedPassword ?? string.Empty;
- return TryNormalizeEmail(requestedEmail, out email) && password.Length > 0;
- }
-
- private static Task<bool> PublisherNameExistsAsync(
- UserManager<ApplicationUser> users,
- string normalizedPublisherName,
- CancellationToken cancellationToken) =>
- users.Users.AnyAsync(
- user => user.NormalizedPublisherName == normalizedPublisherName,
- cancellationToken);
-
- private static bool IsPublisherNameConflict(DbUpdateException exception) =>
- exception.InnerException is PostgresException
- {
- SqlState: PostgresErrorCodes.UniqueViolation,
- ConstraintName: "PublisherNameIndex",
- };
-
- private static bool TryNormalizeEmail(string? value, out string email)
- {
- email = value?.Trim().ToLowerInvariant() ?? string.Empty;
- return System.Net.Mail.MailAddress.TryCreate(email, out System.Net.Mail.MailAddress? address)
- && string.Equals(address.Address, email, StringComparison.OrdinalIgnoreCase);
- }
-
- private static IResult InvalidRequest() =>
- Results.BadRequest(new ApiError("invalid_request", "The request fields are invalid."));
- private static IResult InvalidCredentials() =>
- Results.Json(new ApiError("invalid_credentials", "The email or password is incorrect."),
- statusCode: StatusCodes.Status401Unauthorized);
- private static IResult InvalidPublisherName(string message) =>
- Results.BadRequest(new ApiError("invalid_publisher_name", message));
- private static IResult PublisherNameUnavailable() =>
- Results.Conflict(new ApiError("publisher_name_unavailable", "This username is not available."));
- private static IResult IdentityErrors(IdentityResult result) =>
- Results.BadRequest(new { error = "identity_validation", errors = result.Errors.Select(item => item.Description) });
-}
-
-internal sealed record RegisterRequest(string? Email, string? Password, string? PublisherName);
-internal sealed record Credentials(string? Email, string? Password);
-internal sealed record EmailRequest(string? Email);
-internal sealed record VerificationRequest(string? Email, string? Code, string? PublisherName);
-internal sealed record RecoveryRequest(string? Email, string? Code, string? Password);
-internal sealed record DeleteAccountRequest(string? Confirmation);
-internal sealed record ApiError(string Error, string Message);
diff --git a/backend/Auth/PublisherNamePolicy.cs b/backend/Auth/PublisherNamePolicy.cs
deleted file mode 100644
index 4a4dcfb..0000000
--- a/backend/Auth/PublisherNamePolicy.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.0
-
-namespace XSharp.Web.Api.Auth;
-
-internal static class PublisherNamePolicy
-{
- public const int MinimumLength = 8;
- public const int MaximumLength = 128;
-
- private static readonly string[] ReservedNames = ["Progmasoft", "Leitwolf"];
-
- public static PublisherNameValidation Validate(string? value)
- {
- string publisherName = value ?? string.Empty;
- if (publisherName.Length is < MinimumLength or > MaximumLength)
- {
- return PublisherNameValidation.Invalid(
- $"Username must be between {MinimumLength} and {MaximumLength} characters.");
- }
- if (!IsUpperAscii(publisherName[0]))
- {
- return PublisherNameValidation.Invalid("Username must start with an uppercase ASCII letter.");
- }
-
- // Publisher coordinates are URL path segments. Restricting the complete value to ASCII letters and digits keeps
- // canonical package URLs portable across filesystems, shells, registries, and case-sensitive clients.
- if (!publisherName.All(IsAsciiLetterOrDigit))
- {
- return PublisherNameValidation.Invalid("Username can contain only ASCII letters and digits.");
- }
- if (ReservedNames.Contains(publisherName, StringComparer.OrdinalIgnoreCase))
- {
- return PublisherNameValidation.Invalid("This username is reserved.");
- }
-
- return PublisherNameValidation.Valid(publisherName, publisherName.ToUpperInvariant());
- }
-
- private static bool IsUpperAscii(char value) => value is >= 'A' and <= 'Z';
-
- private static bool IsAsciiLetterOrDigit(char value) =>
- value is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9';
-}
-
-internal readonly record struct PublisherNameValidation(
- bool IsValid,
- string Value,
- string NormalizedValue,
- string? Error)
-{
- public static PublisherNameValidation Valid(string value, string normalizedValue) =>
- new(true, value, normalizedValue, null);
-
- public static PublisherNameValidation Invalid(string error) => new(false, string.Empty, string.Empty, error);
-}
diff --git a/backend/Data/ApplicationUser.cs b/backend/Data/ApplicationUser.cs
deleted file mode 100644
index 6e0dbb5..0000000
--- a/backend/Data/ApplicationUser.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.0
-
-using Microsoft.AspNetCore.Identity;
-
-namespace XSharp.Web.Api.Data;
-
-internal sealed class ApplicationUser : IdentityUser<Guid>
-{
- public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
- public string? PublisherName { get; set; }
- public string? NormalizedPublisherName { get; set; }
-}
diff --git a/backend/Data/AuthChallenge.cs b/backend/Data/AuthChallenge.cs
deleted file mode 100644
index 0a03625..0000000
--- a/backend/Data/AuthChallenge.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.0
-
-namespace XSharp.Web.Api.Data;
-
-internal sealed class AuthChallenge
-{
- public Guid Id { get; init; } = Guid.NewGuid();
- public required Guid UserId { get; init; }
- public required string Purpose { get; init; }
- public required byte[] CodeDigest { get; init; }
- public DateTimeOffset ExpiresAt { get; init; }
- public int FailedAttempts { get; set; }
- public DateTimeOffset? ConsumedAt { get; set; }
-}
diff --git a/backend/Data/Migrations/20260726172221_InitialRegistryIdentity.Designer.cs b/backend/Data/Migrations/20260726172221_InitialRegistryIdentity.Designer.cs
deleted file mode 100644
index c8c7b29..0000000
--- a/backend/Data/Migrations/20260726172221_InitialRegistryIdentity.Designer.cs
+++ /dev/null
@@ -1,365 +0,0 @@
-// <auto-generated />
-// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.0
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-using XSharp.Web.Api.Data;
-
-#nullable disable
-
-namespace XSharp.Web.Api.Data.Migrations
-{
- [DbContext(typeof(RegistryDbContext))]
- [Migration("20260726172221_InitialRegistryIdentity")]
- partial class InitialRegistryIdentity
- {
- /// <inheritdoc />
- protected override void BuildTargetModel(ModelBuilder modelBuilder)
- {
-#pragma warning disable 612, 618
- modelBuilder
- .HasDefaultSchema("registry")
- .HasAnnotation("ProductVersion", "10.0.2")
- .HasAnnotation("Relational:MaxIdentifierLength", 63);
-
- NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
- {
- b.Property<Guid>("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("uuid");
-
- b.Property<string>("ConcurrencyStamp")
- .IsConcurrencyToken()
- .HasColumnType("text");
-
- b.Property<string>("Name")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.Property<string>("NormalizedName")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)");
-
- b.HasKey("Id");
-
- b.HasIndex("NormalizedName")
- .IsUnique()
- .HasDatabaseName("RoleNameIndex");
-
- b.ToTable("AspNetRoles", "registry");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
- {
- b.Property<int>("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("integer");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
-
- b.Property<string>("ClaimType")
- .HasColumnType("text");
-
- b.Property<string>("ClaimValue")
- .HasColumnType("text");
-
- b.Property<Guid>("RoleId")
- .HasColumnType("uuid");
-
- b.HasKey("Id");
-
- b.HasIndex("RoleId");
-
- b.ToTable("AspNetRoleClaims", "registry");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
- {
- b.Property<int>("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("integer");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
-
- b.Property<string>("ClaimType")
- .HasColumnType("text");
-
- b.Property<string>("ClaimValue")
- .HasColumnType("text");
-
- b.Property<Guid>("UserId")
- .HasColumnType("uuid");
-
- b.HasKey("Id");
-
- b.HasIndex("UserId");
-
- b.ToTable("AspNetUserClaims", "registry");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
- {
- b.Property<string>("LoginProvider")
- .HasColumnType("text");
-
- b.Property<string>("ProviderKey")
- .HasColumnType("text");
-
- b.Property<string>("ProviderDisplayName")
- .HasColumnType("text");
-
- b.Property<Guid>("UserId")
- .HasColumnType("uuid");
-
- b.HasKey("LoginProvider", "ProviderKey");
-
- b.HasIndex("UserId");
-
- b.ToTable("AspNetUserLogins", "registry");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
- {
- b.Property<Guid>("UserId")
- .HasColumnType("uuid");
-
- b.Property<Guid>("RoleId")
- .HasColumnType("uuid");
-
- b.HasKey("UserId", "RoleId");
-
- b.HasIndex("RoleId");
-
- b.ToTable("AspNetUserRoles", "registry");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
- {