From d187bf224149c20065b9c5cd4e8fe89ea5bb9462 Mon Sep 17 00:00:00 2001 From: juljanblischke Date: Tue, 20 Jan 2026 12:56:51 +0100 Subject: [PATCH] Revert "feat(frontend): move all system routes to /system/* prefix (#39)" This reverts commit c0e2b9f1f11ceebfe7d2b7506d872080d6a5ad2f. --- .github/workflows/backend.yml | 4 +- .github/workflows/frontend.yml | 4 +- .../ExoAuth.Api/Controllers/AuthController.cs | 63 - backend/src/ExoAuth.Api/ExoAuth.Api.csproj | 2 - .../Extensions/ServiceCollectionExtensions.cs | 6 - .../ExoAuth.Api/appsettings.Development.json | 2 +- backend/src/ExoAuth.Api/appsettings.json | 10 + .../Common/Exceptions/AuthException.cs | 11 - .../Common/Interfaces/IAppDbContext.cs | 1 - .../Common/Interfaces/IAuditService.cs | 5 - .../Common/Interfaces/IEmailService.cs | 17 - .../Common/Interfaces/IMagicLinkService.cs | 48 - .../LoginWithMagicLinkCommand.cs | 16 - .../LoginWithMagicLinkHandler.cs | 462 --- .../LoginWithMagicLinkValidator.cs | 12 - .../RequestMagicLinkCommand.cs | 21 - .../RequestMagicLinkHandler.cs | 95 - .../RequestMagicLinkValidator.cs | 13 - .../ExoAuth.Domain/Entities/MagicLinkToken.cs | 90 - .../appsettings.Development.json | 2 - .../DependencyInjection.cs | 3 - .../Persistence/AppDbContext.cs | 8 +- .../MagicLinkTokenConfiguration.cs | 50 - ...60118174845_AddMagicLinkTokens.Designer.cs | 1652 ---------- .../20260118174845_AddMagicLinkTokens.cs | 67 - .../Migrations/AppDbContextModelSnapshot.cs | 70 - .../Services/EmailService.cs | 31 - .../Services/MagicLinkService.cs | 121 - backend/swagger-verification.md | 90 - .../templates/emails/de-DE/magic-link.html | 80 - backend/templates/emails/de-DE/subjects.json | 3 +- .../templates/emails/en-US/magic-link.html | 80 - backend/templates/emails/en-US/subjects.json | 3 +- backend/test-magic-link.md | 213 -- .../LoginWithMagicLinkHandlerTests.cs | 500 ---- .../Auth/MagicLink/MagicLinkTokenTests.cs | 189 -- .../Auth/MagicLink/MagicLinkValidatorTests.cs | 216 -- .../MagicLink/RequestMagicLinkHandlerTests.cs | 239 -- .../Helpers/MockDbContext.cs | 1 - .../Helpers/TestDataFactory.cs | 36 - frontend/package-lock.json | 2663 ++--------------- .../components/shared/layout/breadcrumbs.tsx | 9 +- .../components/shared/layout/mobile-nav.tsx | 2 +- .../src/components/shared/layout/sidebar.tsx | 4 +- .../components/shared/layout/user-menu.tsx | 4 +- frontend/src/config/navigation.ts | 14 +- frontend/src/contexts/auth-context.tsx | 4 +- frontend/src/features/auth/api/auth-api.ts | 29 - .../auth/components/accept-invite-form.tsx | 2 +- .../src/features/auth/components/index.ts | 2 - .../features/auth/components/login-form.tsx | 4 +- .../auth/components/magic-link-form.tsx | 115 - .../auth/components/magic-link-sent.tsx | 36 - .../auth/components/register-form.tsx | 4 +- frontend/src/features/auth/hooks/index.ts | 4 - .../features/auth/hooks/use-accept-invite.ts | 2 +- frontend/src/features/auth/hooks/use-login.ts | 2 +- .../src/features/auth/hooks/use-logout.ts | 2 +- .../src/features/auth/hooks/use-magic-link.ts | 47 - .../src/features/auth/hooks/use-mfa-verify.ts | 2 +- .../features/auth/hooks/use-passkey-login.ts | 2 +- .../src/features/auth/hooks/use-register.ts | 2 +- .../auth/hooks/use-request-magic-link.ts | 10 - frontend/src/features/auth/index.ts | 9 +- frontend/src/features/auth/types/index.ts | 12 - frontend/src/i18n/locales/de/auth.json | 20 - frontend/src/i18n/locales/de/navigation.json | 2 - frontend/src/i18n/locales/en/auth.json | 24 +- frontend/src/i18n/locales/en/errors.json | 2 - frontend/src/i18n/locales/en/navigation.json | 2 - frontend/src/routes/__root.tsx | 42 +- frontend/src/routes/approve-device.tsx | 6 +- frontend/src/routes/forbidden.tsx | 2 +- frontend/src/routes/index-page.tsx | 2 +- frontend/src/routes/login.tsx | 31 +- frontend/src/routes/magic-link-login.tsx | 273 -- frontend/src/routes/not-found.tsx | 2 +- frontend/src/routes/protected-route.tsx | 4 +- frontend/src/routes/register.tsx | 2 +- frontend/src/routes/reset-password.tsx | 6 +- frontend/src/routes/server-error.tsx | 2 +- frontend/src/types/auth.ts | 15 - frontend/yarn.lock | 648 +++- 83 files changed, 928 insertions(+), 7679 deletions(-) delete mode 100644 backend/src/ExoAuth.Application/Common/Interfaces/IMagicLinkService.cs delete mode 100644 backend/src/ExoAuth.Application/Features/Auth/Commands/LoginWithMagicLink/LoginWithMagicLinkCommand.cs delete mode 100644 backend/src/ExoAuth.Application/Features/Auth/Commands/LoginWithMagicLink/LoginWithMagicLinkHandler.cs delete mode 100644 backend/src/ExoAuth.Application/Features/Auth/Commands/LoginWithMagicLink/LoginWithMagicLinkValidator.cs delete mode 100644 backend/src/ExoAuth.Application/Features/Auth/Commands/RequestMagicLink/RequestMagicLinkCommand.cs delete mode 100644 backend/src/ExoAuth.Application/Features/Auth/Commands/RequestMagicLink/RequestMagicLinkHandler.cs delete mode 100644 backend/src/ExoAuth.Application/Features/Auth/Commands/RequestMagicLink/RequestMagicLinkValidator.cs delete mode 100644 backend/src/ExoAuth.Domain/Entities/MagicLinkToken.cs delete mode 100644 backend/src/ExoAuth.Infrastructure/Persistence/Configurations/MagicLinkTokenConfiguration.cs delete mode 100644 backend/src/ExoAuth.Infrastructure/Persistence/Migrations/20260118174845_AddMagicLinkTokens.Designer.cs delete mode 100644 backend/src/ExoAuth.Infrastructure/Persistence/Migrations/20260118174845_AddMagicLinkTokens.cs delete mode 100644 backend/src/ExoAuth.Infrastructure/Services/MagicLinkService.cs delete mode 100644 backend/swagger-verification.md delete mode 100644 backend/templates/emails/de-DE/magic-link.html delete mode 100644 backend/templates/emails/en-US/magic-link.html delete mode 100644 backend/test-magic-link.md delete mode 100644 backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/LoginWithMagicLinkHandlerTests.cs delete mode 100644 backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/MagicLinkTokenTests.cs delete mode 100644 backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/MagicLinkValidatorTests.cs delete mode 100644 backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/RequestMagicLinkHandlerTests.cs delete mode 100644 frontend/src/features/auth/components/magic-link-form.tsx delete mode 100644 frontend/src/features/auth/components/magic-link-sent.tsx delete mode 100644 frontend/src/features/auth/hooks/use-magic-link.ts delete mode 100644 frontend/src/features/auth/hooks/use-request-magic-link.ts delete mode 100644 frontend/src/routes/magic-link-login.tsx diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 2ef2124..d809205 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -2,12 +2,12 @@ name: Backend CI on: push: - branches: [main, develop] + branches: [main] paths: - 'backend/**' - '.github/workflows/backend.yml' pull_request: - branches: [main, develop] + branches: [main] paths: - 'backend/**' - '.github/workflows/backend.yml' diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index ca03c4c..6078fe3 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -2,12 +2,12 @@ name: Frontend CI on: push: - branches: [main, develop] + branches: [main] paths: - 'frontend/**' - '.github/workflows/frontend.yml' pull_request: - branches: [main, develop] + branches: [main] paths: - 'frontend/**' - '.github/workflows/frontend.yml' diff --git a/backend/src/ExoAuth.Api/Controllers/AuthController.cs b/backend/src/ExoAuth.Api/Controllers/AuthController.cs index da53443..2064e69 100644 --- a/backend/src/ExoAuth.Api/Controllers/AuthController.cs +++ b/backend/src/ExoAuth.Api/Controllers/AuthController.cs @@ -17,8 +17,6 @@ using ExoAuth.Application.Features.Auth.Commands.DenyDevice; using ExoAuth.Application.Features.Auth.Commands.ResendDeviceApproval; using ExoAuth.Application.Features.Auth.Commands.ResendPasswordReset; -using ExoAuth.Application.Features.Auth.Commands.RequestMagicLink; -using ExoAuth.Application.Features.Auth.Commands.LoginWithMagicLink; using ExoAuth.Application.Features.Auth.Models; using ExoAuth.Application.Features.Auth.Queries.GetCurrentUser; using ExoAuth.Application.Features.Auth.Queries.GetDevices; @@ -314,55 +312,6 @@ public async Task ResetPassword(ResetPasswordRequest request, Can return ApiOk(result); } - /// - /// Request a magic link email for passwordless login. - /// - [HttpPost("magic-link/request")] - [RateLimit("forgot-password")] - [ProducesResponseType(typeof(RequestMagicLinkResponse), StatusCodes.Status200OK)] - public async Task RequestMagicLink(RequestMagicLinkRequest request, CancellationToken ct) - { - var command = new RequestMagicLinkCommand( - request.Email, - request.CaptchaToken, - HttpContext.Connection.RemoteIpAddress?.ToString() - ); - - var result = await Mediator.Send(command, ct); - - return ApiOk(result); - } - - /// - /// Login with magic link token. - /// - [HttpPost("magic-link/login")] - [RateLimit("sensitive")] - [ProducesResponseType(typeof(AuthResponse), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task MagicLinkLogin(MagicLinkLoginRequest request, CancellationToken ct) - { - var command = new LoginWithMagicLinkCommand( - request.Token, - request.DeviceId, - request.DeviceFingerprint, - Request.Headers.UserAgent.ToString(), - HttpContext.Connection.RemoteIpAddress?.ToString(), - request.RememberMe - ); - - var result = await Mediator.Send(command, ct); - - // Only set cookies when login is complete (not during MFA or device approval flow) - if (!result.MfaRequired && !result.MfaSetupRequired && !result.DeviceApprovalRequired) - { - SetAuthCookies(result.AccessToken!, result.RefreshToken!); - } - - return ApiOk(result); - } - #region Devices /// @@ -864,18 +813,6 @@ public sealed record ResetPasswordRequest( string NewPassword ); -public sealed record RequestMagicLinkRequest( - string Email, - string? CaptchaToken = null -); - -public sealed record MagicLinkLoginRequest( - string Token, - string? DeviceId = null, - string? DeviceFingerprint = null, - bool RememberMe = false -); - public sealed record MfaSetupRequest( string? SetupToken = null ); diff --git a/backend/src/ExoAuth.Api/ExoAuth.Api.csproj b/backend/src/ExoAuth.Api/ExoAuth.Api.csproj index 3c94e3f..135fb13 100644 --- a/backend/src/ExoAuth.Api/ExoAuth.Api.csproj +++ b/backend/src/ExoAuth.Api/ExoAuth.Api.csproj @@ -4,8 +4,6 @@ net8.0 enable enable - true - $(NoWarn);1591 diff --git a/backend/src/ExoAuth.Api/Extensions/ServiceCollectionExtensions.cs b/backend/src/ExoAuth.Api/Extensions/ServiceCollectionExtensions.cs index f8d0907..388ffcc 100644 --- a/backend/src/ExoAuth.Api/Extensions/ServiceCollectionExtensions.cs +++ b/backend/src/ExoAuth.Api/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,3 @@ -using System.Reflection; using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; @@ -19,11 +18,6 @@ public static IServiceCollection AddSwaggerConfiguration(this IServiceCollection Description = "Authentication and Authorization API" }); - // Include XML documentation - var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); - options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true); - options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Name = "Authorization", diff --git a/backend/src/ExoAuth.Api/appsettings.Development.json b/backend/src/ExoAuth.Api/appsettings.Development.json index 8d8a893..b4eaf3f 100644 --- a/backend/src/ExoAuth.Api/appsettings.Development.json +++ b/backend/src/ExoAuth.Api/appsettings.Development.json @@ -17,7 +17,7 @@ "Secure": false }, "Captcha": { - "Enabled": false + "Enabled": true }, "Serilog": { "MinimumLevel": { diff --git a/backend/src/ExoAuth.Api/appsettings.json b/backend/src/ExoAuth.Api/appsettings.json index 872edd5..3fd0076 100644 --- a/backend/src/ExoAuth.Api/appsettings.json +++ b/backend/src/ExoAuth.Api/appsettings.json @@ -19,6 +19,16 @@ "RefreshTokenExpirationDays": 7, "RememberMeExpirationDays": 30 }, + "Email": { + "Provider": "SMTP", + "SmtpHost": "smtp.example.com", + "SmtpPort": 587, + "SmtpUsername": "", + "SmtpPassword": "", + "SmtpUseSsl": true, + "FromEmail": "noreply@exoauth.com", + "FromName": "ExoAuth" + }, "SystemInvite": { "ExpirationHours": 24, "BaseUrl": "http://localhost:5173" diff --git a/backend/src/ExoAuth.Application/Common/Exceptions/AuthException.cs b/backend/src/ExoAuth.Application/Common/Exceptions/AuthException.cs index c937a57..396c337 100644 --- a/backend/src/ExoAuth.Application/Common/Exceptions/AuthException.cs +++ b/backend/src/ExoAuth.Application/Common/Exceptions/AuthException.cs @@ -494,14 +494,3 @@ public CaptchaExpiredException() { } } - -/// -/// Exception when magic link token is invalid. -/// -public sealed class MagicLinkTokenInvalidException : AuthException -{ - public MagicLinkTokenInvalidException() - : base("MAGIC_LINK_TOKEN_INVALID", "Invalid or expired magic link token", 400) - { - } -} diff --git a/backend/src/ExoAuth.Application/Common/Interfaces/IAppDbContext.cs b/backend/src/ExoAuth.Application/Common/Interfaces/IAppDbContext.cs index b1858b8..344ec5c 100644 --- a/backend/src/ExoAuth.Application/Common/Interfaces/IAppDbContext.cs +++ b/backend/src/ExoAuth.Application/Common/Interfaces/IAppDbContext.cs @@ -15,7 +15,6 @@ public interface IAppDbContext DbSet SystemInvites { get; } DbSet RefreshTokens { get; } DbSet PasswordResetTokens { get; } - DbSet MagicLinkTokens { get; } DbSet MfaBackupCodes { get; } DbSet LoginPatterns { get; } DbSet Devices { get; } diff --git a/backend/src/ExoAuth.Application/Common/Interfaces/IAuditService.cs b/backend/src/ExoAuth.Application/Common/Interfaces/IAuditService.cs index df22b53..766da35 100644 --- a/backend/src/ExoAuth.Application/Common/Interfaces/IAuditService.cs +++ b/backend/src/ExoAuth.Application/Common/Interfaces/IAuditService.cs @@ -79,11 +79,6 @@ public static class AuditActions public const string PasswordResetCompleted = "system.password.reset_completed"; public const string PasswordChanged = "system.password.changed"; - // Magic link actions - public const string MagicLinkRequested = "system.magic_link.requested"; - public const string MagicLinkLogin = "system.magic_link.login"; - public const string MagicLinkLoginFailed = "system.magic_link.login_failed"; - // Session actions public const string SessionCreated = "system.session.created"; public const string SessionRevoked = "system.session.revoked"; diff --git a/backend/src/ExoAuth.Application/Common/Interfaces/IEmailService.cs b/backend/src/ExoAuth.Application/Common/Interfaces/IEmailService.cs index 6db96a0..fc45a31 100644 --- a/backend/src/ExoAuth.Application/Common/Interfaces/IEmailService.cs +++ b/backend/src/ExoAuth.Application/Common/Interfaces/IEmailService.cs @@ -58,23 +58,6 @@ Task SendPasswordResetAsync( string language = "en-US", CancellationToken cancellationToken = default); - /// - /// Sends a magic link email for passwordless login. - /// - /// The recipient email address. - /// The recipient's first name. - /// The magic link token for authentication. - /// The user ID for tracking. - /// The language for the template (default: "en-US"). - /// Cancellation token. - Task SendMagicLinkAsync( - string email, - string firstName, - string magicLinkToken, - Guid userId, - string language = "en-US", - CancellationToken cancellationToken = default); - /// /// Sends a password changed confirmation email. /// diff --git a/backend/src/ExoAuth.Application/Common/Interfaces/IMagicLinkService.cs b/backend/src/ExoAuth.Application/Common/Interfaces/IMagicLinkService.cs deleted file mode 100644 index 27f1262..0000000 --- a/backend/src/ExoAuth.Application/Common/Interfaces/IMagicLinkService.cs +++ /dev/null @@ -1,48 +0,0 @@ -using ExoAuth.Domain.Entities; - -namespace ExoAuth.Application.Common.Interfaces; - -/// -/// Service for managing magic link tokens. -/// -public interface IMagicLinkService -{ - /// - /// Creates a new magic link token for a user. - /// Generates a URL token with collision prevention. - /// - /// The user ID. - /// Cancellation token. - /// The created token entity and the plain text token. - Task CreateMagicLinkAsync(Guid userId, CancellationToken cancellationToken = default); - - /// - /// Validates a magic link token. - /// - /// The plain text token. - /// Cancellation token. - /// The token entity if valid, null otherwise. - Task ValidateTokenAsync(string token, CancellationToken cancellationToken = default); - - /// - /// Marks a magic link token as used. - /// - /// The token entity. - /// Cancellation token. - Task MarkAsUsedAsync(MagicLinkToken token, CancellationToken cancellationToken = default); - - /// - /// Invalidates all pending magic link tokens for a user. - /// - /// The user ID. - /// Cancellation token. - Task InvalidateAllTokensAsync(Guid userId, CancellationToken cancellationToken = default); -} - -/// -/// Result of creating a magic link token. -/// -public sealed record MagicLinkResult( - MagicLinkToken Entity, - string Token -); diff --git a/backend/src/ExoAuth.Application/Features/Auth/Commands/LoginWithMagicLink/LoginWithMagicLinkCommand.cs b/backend/src/ExoAuth.Application/Features/Auth/Commands/LoginWithMagicLink/LoginWithMagicLinkCommand.cs deleted file mode 100644 index 766341a..0000000 --- a/backend/src/ExoAuth.Application/Features/Auth/Commands/LoginWithMagicLink/LoginWithMagicLinkCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -using ExoAuth.Application.Features.Auth.Models; -using Mediator; - -namespace ExoAuth.Application.Features.Auth.Commands.LoginWithMagicLink; - -/// -/// Command to login a user with a magic link token. -/// -public sealed record LoginWithMagicLinkCommand( - string Token, - string? DeviceId = null, - string? DeviceFingerprint = null, - string? UserAgent = null, - string? IpAddress = null, - bool RememberMe = false -) : ICommand; diff --git a/backend/src/ExoAuth.Application/Features/Auth/Commands/LoginWithMagicLink/LoginWithMagicLinkHandler.cs b/backend/src/ExoAuth.Application/Features/Auth/Commands/LoginWithMagicLink/LoginWithMagicLinkHandler.cs deleted file mode 100644 index 1a536a2..0000000 --- a/backend/src/ExoAuth.Application/Features/Auth/Commands/LoginWithMagicLink/LoginWithMagicLinkHandler.cs +++ /dev/null @@ -1,462 +0,0 @@ -using ExoAuth.Application.Common.Exceptions; -using ExoAuth.Application.Common.Interfaces; -using ExoAuth.Application.Common.Models; -using ExoAuth.Application.Features.Auth.Models; -using ExoAuth.Domain.Enums; -using Mediator; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace ExoAuth.Application.Features.Auth.Commands.LoginWithMagicLink; - -public sealed class LoginWithMagicLinkHandler : ICommandHandler -{ - private readonly IAppDbContext _context; - private readonly IMagicLinkService _magicLinkService; - private readonly ISystemUserRepository _userRepository; - private readonly ITokenService _tokenService; - private readonly IPermissionCacheService _permissionCache; - private readonly IForceReauthService _forceReauthService; - private readonly IRevokedSessionService _revokedSessionService; - private readonly IDeviceService _deviceService; - private readonly IAuditService _auditService; - private readonly IMfaService _mfaService; - private readonly IEmailService _emailService; - private readonly IEmailTemplateService _emailTemplateService; - private readonly IConfiguration _configuration; - private readonly IRiskScoringService _riskScoringService; - private readonly ILoginPatternService _loginPatternService; - private readonly IGeoIpService _geoIpService; - private readonly IDeviceDetectionService _deviceDetectionService; - private readonly ILogger _logger; - - public LoginWithMagicLinkHandler( - IAppDbContext context, - IMagicLinkService magicLinkService, - ISystemUserRepository userRepository, - ITokenService tokenService, - IPermissionCacheService permissionCache, - IForceReauthService forceReauthService, - IRevokedSessionService revokedSessionService, - IDeviceService deviceService, - IAuditService auditService, - IMfaService mfaService, - IEmailService emailService, - IEmailTemplateService emailTemplateService, - IConfiguration configuration, - IRiskScoringService riskScoringService, - ILoginPatternService loginPatternService, - IGeoIpService geoIpService, - IDeviceDetectionService deviceDetectionService, - ILogger logger) - { - _context = context; - _magicLinkService = magicLinkService; - _userRepository = userRepository; - _tokenService = tokenService; - _permissionCache = permissionCache; - _forceReauthService = forceReauthService; - _revokedSessionService = revokedSessionService; - _deviceService = deviceService; - _auditService = auditService; - _mfaService = mfaService; - _emailService = emailService; - _emailTemplateService = emailTemplateService; - _configuration = configuration; - _riskScoringService = riskScoringService; - _loginPatternService = loginPatternService; - _geoIpService = geoIpService; - _deviceDetectionService = deviceDetectionService; - _logger = logger; - } - - public async ValueTask Handle(LoginWithMagicLinkCommand command, CancellationToken ct) - { - // Validate magic link token - var magicLinkToken = await _magicLinkService.ValidateTokenAsync(command.Token, ct); - - if (magicLinkToken is null) - { - await _auditService.LogWithContextAsync( - AuditActions.MagicLinkLoginFailed, - null, - null, - null, - null, - new { Reason = "Invalid or expired token" }, - ct - ); - - throw new MagicLinkTokenInvalidException(); - } - - // Get the user - var user = await _userRepository.GetByIdAsync(magicLinkToken.UserId, ct); - - if (user is null || !user.IsActive || user.IsLocked) - { - await _auditService.LogWithContextAsync( - AuditActions.MagicLinkLoginFailed, - user?.Id, - null, - "SystemUser", - user?.Id, - new { Reason = user is null ? "User not found" : user.IsLocked ? "Account locked" : "User inactive" }, - ct - ); - - throw new MagicLinkTokenInvalidException(); - } - - // Mark token as used - await _magicLinkService.MarkAsUsedAsync(magicLinkToken, ct); - - // Get permissions (with caching) - var permissions = await _permissionCache.GetOrSetPermissionsAsync( - user.Id, - () => _userRepository.GetUserPermissionNamesAsync(user.Id, ct), - ct - ); - - // Check if MFA is enabled - if (user.MfaEnabled) - { - // Generate MFA token for the second step - var mfaToken = _mfaService.GenerateMfaToken(user.Id, null); - - await _auditService.LogWithContextAsync( - AuditActions.MfaChallengeSent, - user.Id, - null, - "SystemUser", - user.Id, - new { Step = "awaiting_mfa", Method = "magic_link" }, - ct - ); - - return AuthResponse.RequiresMfa(mfaToken); - } - - // Check if user has system permissions but MFA is not enabled - // Users with system permissions MUST have MFA enabled - var hasSystemPermissions = permissions.Any(p => p.StartsWith("system:")); - if (hasSystemPermissions && !user.MfaEnabled) - { - // Generate setup token for MFA setup - var setupToken = _mfaService.GenerateMfaToken(user.Id, null); - - await _auditService.LogWithContextAsync( - AuditActions.MfaSetupRequiredSent, - user.Id, - null, - "SystemUser", - user.Id, - new { Step = "awaiting_mfa_setup", Method = "magic_link" }, - ct - ); - - return AuthResponse.RequiresMfaSetup(setupToken); - } - - // Get geo location and device info (needed for trust check and device creation) - var geoLocation = _geoIpService.GetLocation(command.IpAddress); - var deviceInfo = _deviceDetectionService.Parse(command.UserAgent); - var deviceId = command.DeviceId ?? _deviceService.GenerateDeviceId(); - - // Check if this device is trusted - var device = await _deviceService.FindTrustedDeviceAsync( - user.Id, - deviceId, - command.DeviceFingerprint, - ct - ); - - // NEW DEVICE → Always require approval - if (device is null) - { - // Calculate risk score for email/audit purposes - var riskScore = await _riskScoringService.CalculateAsync( - user.Id, - deviceInfo, - geoLocation, - false, // Not trusted - ct - ); - - // Create pending device with approval credentials - var pendingResult = await _deviceService.CreatePendingDeviceAsync( - user.Id, - deviceId, - riskScore.Score, - riskScore.Factors, - deviceInfo, - geoLocation, - command.DeviceFingerprint, - ct - ); - - var pendingDevice = pendingResult.Device; - - // Send device approval email - await _emailService.SendDeviceApprovalRequiredAsync( - email: user.Email, - firstName: user.FirstName, - approvalToken: pendingResult.ApprovalToken, - approvalCode: pendingResult.ApprovalCode, - deviceName: pendingDevice.DisplayName, - browser: pendingDevice.Browser, - operatingSystem: pendingDevice.OperatingSystem, - location: pendingDevice.LocationDisplay, - ipAddress: pendingDevice.IpAddress, - riskScore: riskScore.Score, - userId: user.Id, - language: user.PreferredLanguage, - cancellationToken: ct - ); - - // Audit log - await _auditService.LogWithContextAsync( - AuditActions.DeviceApprovalRequired, - user.Id, - null, - "Device", - pendingDevice.Id, - new - { - riskScore.Score, - riskScore.Level, - riskScore.Factors, - DeviceId = deviceId, - Reason = "New device requires approval", - Method = "magic_link" - }, - ct - ); - - return AuthResponse.RequiresDeviceApproval( - approvalToken: pendingResult.ApprovalToken, - sessionId: pendingDevice.Id, - deviceId: deviceId, - riskScore: riskScore.Score, - riskLevel: riskScore.Level.ToString(), - riskFactors: riskScore.Factors.ToList() - ); - } - - // Check if location has changed - var isNewLocation = !string.Equals(device.CountryCode, geoLocation.CountryCode, StringComparison.OrdinalIgnoreCase) - || !string.Equals(device.City, geoLocation.City, StringComparison.OrdinalIgnoreCase); - - // TRUSTED DEVICE → Check for spoofing - var spoofingCheck = await _riskScoringService.CheckForSpoofingAsync( - user.Id, - device, - geoLocation, - deviceInfo, - ct - ); - - // If suspicious activity detected on trusted device, require re-verification - if (spoofingCheck.IsSuspicious) - { - // Create a new pending device for re-approval - var pendingResult = await _deviceService.CreatePendingDeviceAsync( - user.Id, - deviceId, - spoofingCheck.RiskScore, - spoofingCheck.SuspiciousFactors, - deviceInfo, - geoLocation, - command.DeviceFingerprint, - ct - ); - - var pendingDevice = pendingResult.Device; - - // Send device approval email - await _emailService.SendDeviceApprovalRequiredAsync( - email: user.Email, - firstName: user.FirstName, - approvalToken: pendingResult.ApprovalToken, - approvalCode: pendingResult.ApprovalCode, - deviceName: pendingDevice.DisplayName, - browser: pendingDevice.Browser, - operatingSystem: pendingDevice.OperatingSystem, - location: pendingDevice.LocationDisplay, - ipAddress: pendingDevice.IpAddress, - riskScore: spoofingCheck.RiskScore, - userId: user.Id, - language: user.PreferredLanguage, - cancellationToken: ct - ); - - // Audit log - await _auditService.LogWithContextAsync( - AuditActions.DeviceApprovalRequired, - user.Id, - null, - "Device", - pendingDevice.Id, - new - { - Score = spoofingCheck.RiskScore, - Level = "Suspicious", - Factors = spoofingCheck.SuspiciousFactors, - DeviceId = deviceId, - IsNewLocation = isNewLocation, - Reason = "Suspicious activity on trusted device", - Method = "magic_link" - }, - ct - ); - - return AuthResponse.RequiresDeviceApproval( - approvalToken: pendingResult.ApprovalToken, - sessionId: pendingDevice.Id, - deviceId: deviceId, - riskScore: spoofingCheck.RiskScore, - riskLevel: "Suspicious", - riskFactors: spoofingCheck.SuspiciousFactors.ToList() - ); - } - - // Record device usage (updates last used timestamp and location) - await _deviceService.RecordUsageAsync(device.Id, command.IpAddress, geoLocation.CountryCode, geoLocation.City, ct); - - // Clear force re-auth flag and revoked session status for this device - await _forceReauthService.ClearFlagAsync(device.Id, ct); - await _revokedSessionService.ClearRevokedSessionAsync(device.Id, ct); - - // Generate tokens with device ID as session ID - var accessToken = _tokenService.GenerateAccessToken( - user.Id, - user.Email, - UserType.System, - permissions, - device.Id - ); - - var refreshTokenString = _tokenService.GenerateRefreshToken(); - var refreshToken = global::ExoAuth.Domain.Entities.RefreshToken.Create( - userId: user.Id, - userType: UserType.System, - token: refreshTokenString, - expirationDays: command.RememberMe ? _tokenService.RememberMeExpirationDays : (int)_tokenService.RefreshTokenExpiration.TotalDays - ); - - // Link refresh token to device - refreshToken.LinkToDevice(device.Id); - - await _context.RefreshTokens.AddAsync(refreshToken, ct); - await _context.SaveChangesAsync(ct); - - // Record login - user.RecordLogin(); - await _userRepository.UpdateAsync(user, ct); - - // Record login pattern for future risk scoring - await _loginPatternService.RecordLoginAsync( - user.Id, - geoLocation, - deviceInfo.DeviceType, - command.IpAddress, - ct - ); - - // Audit log - await _auditService.LogWithContextAsync( - AuditActions.MagicLinkLogin, - user.Id, - null, - "SystemUser", - user.Id, - new - { - SessionId = device.Id, - DeviceId = deviceId, - IsNewLocation = isNewLocation, - RememberMe = command.RememberMe, - IsTrustedDevice = true, - Method = "magic_link" - }, - ct - ); - - // Send notification email for new location (only for trusted device logins) - if (isNewLocation) - { - await _auditService.LogWithContextAsync( - AuditActions.LoginNewLocation, - user.Id, - null, - "Device", - device.Id, - new { Country = geoLocation.CountryCode, City = geoLocation.City, Method = "magic_link" }, - ct - ); - - await SendNewLocationEmailAsync(user, device, geoLocation, ct); - } - - _logger.LogInformation("User {UserId} logged in successfully with magic link", user.Id); - - return new AuthResponse( - User: new UserDto( - Id: user.Id, - Email: user.Email, - FirstName: user.FirstName, - LastName: user.LastName, - FullName: user.FullName, - IsActive: user.IsActive, - EmailVerified: user.EmailVerified, - MfaEnabled: user.MfaEnabled, - PreferredLanguage: user.PreferredLanguage, - LastLoginAt: user.LastLoginAt, - CreatedAt: user.CreatedAt, - Permissions: permissions - ), - AccessToken: accessToken, - RefreshToken: refreshTokenString, - SessionId: device.Id, - DeviceId: deviceId, - IsNewDevice: false, // We found a trusted device, so not new - IsNewLocation: isNewLocation - ); - } - - private async Task SendNewLocationEmailAsync( - Domain.Entities.SystemUser user, - Domain.Entities.Device device, - GeoLocation newLocation, - CancellationToken ct) - { - var baseUrl = _configuration["SystemInvite:BaseUrl"] ?? "http://localhost:5173"; - var deviceName = device.DisplayName; - var locationDisplay = !string.IsNullOrEmpty(newLocation.City) && !string.IsNullOrEmpty(newLocation.Country) - ? $"{newLocation.City}, {newLocation.Country}" - : newLocation.Country ?? "Unknown Location"; - - var variables = new Dictionary - { - ["firstName"] = user.FirstName, - ["newLocation"] = locationDisplay, - ["previousLocation"] = device.LocationDisplay ?? "Your usual location", - ["deviceName"] = deviceName, - ["ipAddress"] = device.IpAddress ?? "Unknown", - ["loginTime"] = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm 'UTC'"), - ["sessionsUrl"] = $"{baseUrl}/settings/sessions", - ["changePasswordUrl"] = $"{baseUrl}/settings/security", - ["year"] = DateTime.UtcNow.Year.ToString() - }; - - await _emailService.SendAsync( - to: user.Email, - subject: _emailTemplateService.GetSubject("new-location-login", user.PreferredLanguage), - templateName: "new-location-login", - variables: variables, - language: user.PreferredLanguage, - recipientUserId: user.Id, - cancellationToken: ct - ); - } -} diff --git a/backend/src/ExoAuth.Application/Features/Auth/Commands/LoginWithMagicLink/LoginWithMagicLinkValidator.cs b/backend/src/ExoAuth.Application/Features/Auth/Commands/LoginWithMagicLink/LoginWithMagicLinkValidator.cs deleted file mode 100644 index 6de1cdc..0000000 --- a/backend/src/ExoAuth.Application/Features/Auth/Commands/LoginWithMagicLink/LoginWithMagicLinkValidator.cs +++ /dev/null @@ -1,12 +0,0 @@ -using FluentValidation; - -namespace ExoAuth.Application.Features.Auth.Commands.LoginWithMagicLink; - -public sealed class LoginWithMagicLinkValidator : AbstractValidator -{ - public LoginWithMagicLinkValidator() - { - RuleFor(x => x.Token) - .NotEmpty().WithMessage("Magic link token is required"); - } -} diff --git a/backend/src/ExoAuth.Application/Features/Auth/Commands/RequestMagicLink/RequestMagicLinkCommand.cs b/backend/src/ExoAuth.Application/Features/Auth/Commands/RequestMagicLink/RequestMagicLinkCommand.cs deleted file mode 100644 index 30fe243..0000000 --- a/backend/src/ExoAuth.Application/Features/Auth/Commands/RequestMagicLink/RequestMagicLinkCommand.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator; - -namespace ExoAuth.Application.Features.Auth.Commands.RequestMagicLink; - -/// -/// Command to request a magic link email for passwordless login. -/// -public sealed record RequestMagicLinkCommand( - string Email, - string? CaptchaToken = null, - string? IpAddress = null -) : ICommand; - -/// -/// Response for magic link request. -/// Always returns success to prevent email enumeration. -/// -public sealed record RequestMagicLinkResponse( - bool Success, - string Message -); diff --git a/backend/src/ExoAuth.Application/Features/Auth/Commands/RequestMagicLink/RequestMagicLinkHandler.cs b/backend/src/ExoAuth.Application/Features/Auth/Commands/RequestMagicLink/RequestMagicLinkHandler.cs deleted file mode 100644 index aa9555b..0000000 --- a/backend/src/ExoAuth.Application/Features/Auth/Commands/RequestMagicLink/RequestMagicLinkHandler.cs +++ /dev/null @@ -1,95 +0,0 @@ -using ExoAuth.Application.Common.Interfaces; -using Mediator; -using Microsoft.Extensions.Logging; - -namespace ExoAuth.Application.Features.Auth.Commands.RequestMagicLink; - -public sealed class RequestMagicLinkHandler : ICommandHandler -{ - private readonly ISystemUserRepository _userRepository; - private readonly IMagicLinkService _magicLinkService; - private readonly IEmailService _emailService; - private readonly IAuditService _auditService; - private readonly ICaptchaService _captchaService; - private readonly ILogger _logger; - - public RequestMagicLinkHandler( - ISystemUserRepository userRepository, - IMagicLinkService magicLinkService, - IEmailService emailService, - IAuditService auditService, - ICaptchaService captchaService, - ILogger logger) - { - _userRepository = userRepository; - _magicLinkService = magicLinkService; - _emailService = emailService; - _auditService = auditService; - _captchaService = captchaService; - _logger = logger; - } - - public async ValueTask Handle(RequestMagicLinkCommand command, CancellationToken ct) - { - // Validate CAPTCHA (always required for magic link) - await _captchaService.ValidateRequiredAsync( - command.CaptchaToken, - "magic_link", - command.IpAddress, - ct); - - var email = command.Email.ToLowerInvariant(); - - // Always return success to prevent email enumeration - var successResponse = new RequestMagicLinkResponse(true, "If an account exists with this email, you will receive a magic link."); - - // Find user - var user = await _userRepository.GetByEmailAsync(email, ct); - - if (user is null) - { - _logger.LogDebug("Magic link requested for non-existent email: {Email}", email); - return successResponse; - } - - if (!user.IsActive) - { - _logger.LogDebug("Magic link requested for inactive user: {Email}", email); - return successResponse; - } - - if (user.IsAnonymized) - { - _logger.LogDebug("Magic link requested for anonymized user: {Email}", email); - return successResponse; - } - - // Create magic link token - var result = await _magicLinkService.CreateMagicLinkAsync(user.Id, ct); - - // Send email - await _emailService.SendMagicLinkAsync( - email: user.Email, - firstName: user.FirstName, - magicLinkToken: result.Token, - userId: user.Id, - language: user.PreferredLanguage, - cancellationToken: ct - ); - - // Audit log - await _auditService.LogWithContextAsync( - AuditActions.MagicLinkRequested, - user.Id, - null, - "SystemUser", - user.Id, - new { Email = email }, - ct - ); - - _logger.LogInformation("Magic link email sent to {Email}", email); - - return successResponse; - } -} diff --git a/backend/src/ExoAuth.Application/Features/Auth/Commands/RequestMagicLink/RequestMagicLinkValidator.cs b/backend/src/ExoAuth.Application/Features/Auth/Commands/RequestMagicLink/RequestMagicLinkValidator.cs deleted file mode 100644 index f741970..0000000 --- a/backend/src/ExoAuth.Application/Features/Auth/Commands/RequestMagicLink/RequestMagicLinkValidator.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FluentValidation; - -namespace ExoAuth.Application.Features.Auth.Commands.RequestMagicLink; - -public sealed class RequestMagicLinkValidator : AbstractValidator -{ - public RequestMagicLinkValidator() - { - RuleFor(x => x.Email) - .NotEmpty().WithMessage("Email is required") - .EmailAddress().WithMessage("Invalid email format"); - } -} diff --git a/backend/src/ExoAuth.Domain/Entities/MagicLinkToken.cs b/backend/src/ExoAuth.Domain/Entities/MagicLinkToken.cs deleted file mode 100644 index 8282d76..0000000 --- a/backend/src/ExoAuth.Domain/Entities/MagicLinkToken.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Security.Cryptography; -using System.Text; - -namespace ExoAuth.Domain.Entities; - -/// -/// Represents a magic link authentication token for a system user. -/// Provides passwordless authentication via time-limited, single-use tokens. -/// -public sealed class MagicLinkToken : BaseEntity -{ - public Guid UserId { get; private set; } - public string TokenHash { get; private set; } = null!; - public DateTime ExpiresAt { get; private set; } - public bool IsUsed { get; private set; } - public DateTime? UsedAt { get; private set; } - - // Navigation property - public SystemUser? User { get; set; } - - private MagicLinkToken() { } // EF Core - - /// - /// Creates a new magic link token for passwordless authentication. - /// - /// The user ID. - /// The generated token (will be hashed). - /// Expiration time in minutes (default: 15). - public static MagicLinkToken Create( - Guid userId, - string token, - int expirationMinutes = 15) - { - return new MagicLinkToken - { - UserId = userId, - TokenHash = HashValue(token), - ExpiresAt = DateTime.UtcNow.AddMinutes(expirationMinutes), - IsUsed = false - }; - } - - /// - /// Checks if the token is expired. - /// - public bool IsExpired => DateTime.UtcNow > ExpiresAt; - - /// - /// Checks if the token is still valid (not used and not expired). - /// - public bool IsValid => !IsUsed && !IsExpired; - - /// - /// Validates the provided token against the stored hash. - /// - public bool ValidateToken(string token) - { - return TokenHash == HashValue(token); - } - - /// - /// Marks the token as used. - /// - public void MarkAsUsed() - { - IsUsed = true; - UsedAt = DateTime.UtcNow; - SetUpdated(); - } - - /// - /// Generates a cryptographically secure token. - /// - public static string GenerateToken() - { - var bytes = new byte[32]; - using var rng = RandomNumberGenerator.Create(); - rng.GetBytes(bytes); - return Convert.ToBase64String(bytes) - .Replace("+", "-") - .Replace("/", "_") - .TrimEnd('='); - } - - private static string HashValue(string value) - { - var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(value)); - return Convert.ToBase64String(bytes); - } -} diff --git a/backend/src/ExoAuth.EmailWorker/appsettings.Development.json b/backend/src/ExoAuth.EmailWorker/appsettings.Development.json index be537d8..9518c7b 100644 --- a/backend/src/ExoAuth.EmailWorker/appsettings.Development.json +++ b/backend/src/ExoAuth.EmailWorker/appsettings.Development.json @@ -9,8 +9,6 @@ } }, "ConnectionStrings": { - "Database": "Host=localhost;Database=exoauth;Username=exoauth;Password=exoauth_secret", - "Redis": "localhost:6379", "RabbitMq": "amqp://guest:guest@localhost:5672" }, "Email": { diff --git a/backend/src/ExoAuth.Infrastructure/DependencyInjection.cs b/backend/src/ExoAuth.Infrastructure/DependencyInjection.cs index 48c7385..d18a4e7 100644 --- a/backend/src/ExoAuth.Infrastructure/DependencyInjection.cs +++ b/backend/src/ExoAuth.Infrastructure/DependencyInjection.cs @@ -94,9 +94,6 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi // Password Reset services.AddScoped(); - // Magic Link - services.AddScoped(); - // System Invite services.AddScoped(); diff --git a/backend/src/ExoAuth.Infrastructure/Persistence/AppDbContext.cs b/backend/src/ExoAuth.Infrastructure/Persistence/AppDbContext.cs index 2abb57d..f43f08c 100644 --- a/backend/src/ExoAuth.Infrastructure/Persistence/AppDbContext.cs +++ b/backend/src/ExoAuth.Infrastructure/Persistence/AppDbContext.cs @@ -6,12 +6,7 @@ namespace ExoAuth.Infrastructure.Persistence; public class AppDbContext : DbContext, IAppDbContext { - public AppDbContext(DbContextOptions options) : base(options) - { - } - - // Protected constructor for derived classes (e.g., ProDbContext) - protected AppDbContext(DbContextOptions options) : base(options) + public AppDbContext(DbContextOptions options) : base(options) { } @@ -23,7 +18,6 @@ protected AppDbContext(DbContextOptions options) : base(options) public DbSet SystemInvites => Set(); public DbSet RefreshTokens => Set(); public DbSet PasswordResetTokens => Set(); - public DbSet MagicLinkTokens => Set(); public DbSet MfaBackupCodes => Set(); public DbSet LoginPatterns => Set(); public DbSet Devices => Set(); diff --git a/backend/src/ExoAuth.Infrastructure/Persistence/Configurations/MagicLinkTokenConfiguration.cs b/backend/src/ExoAuth.Infrastructure/Persistence/Configurations/MagicLinkTokenConfiguration.cs deleted file mode 100644 index 59c0d8c..0000000 --- a/backend/src/ExoAuth.Infrastructure/Persistence/Configurations/MagicLinkTokenConfiguration.cs +++ /dev/null @@ -1,50 +0,0 @@ -using ExoAuth.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace ExoAuth.Infrastructure.Persistence.Configurations; - -public sealed class MagicLinkTokenConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("magic_link_tokens"); - - builder.HasKey(x => x.Id); - - builder.Property(x => x.UserId) - .IsRequired(); - - builder.Property(x => x.TokenHash) - .IsRequired() - .HasMaxLength(64); - - builder.Property(x => x.ExpiresAt) - .IsRequired(); - - builder.Property(x => x.IsUsed) - .IsRequired() - .HasDefaultValue(false); - - builder.Property(x => x.UsedAt); - - builder.Property(x => x.CreatedAt) - .IsRequired(); - - builder.Property(x => x.UpdatedAt); - - // Indexes - unique constraint on TokenHash to prevent collisions - builder.HasIndex(x => x.TokenHash) - .IsUnique(); - - builder.HasIndex(x => x.UserId); - builder.HasIndex(x => x.ExpiresAt); - builder.HasIndex(x => new { x.UserId, x.IsUsed, x.ExpiresAt }); - - // Relationship - builder.HasOne(x => x.User) - .WithMany() - .HasForeignKey(x => x.UserId) - .OnDelete(DeleteBehavior.Cascade); - } -} diff --git a/backend/src/ExoAuth.Infrastructure/Persistence/Migrations/20260118174845_AddMagicLinkTokens.Designer.cs b/backend/src/ExoAuth.Infrastructure/Persistence/Migrations/20260118174845_AddMagicLinkTokens.Designer.cs deleted file mode 100644 index 31fe14b..0000000 --- a/backend/src/ExoAuth.Infrastructure/Persistence/Migrations/20260118174845_AddMagicLinkTokens.Designer.cs +++ /dev/null @@ -1,1652 +0,0 @@ -// -using System; -using System.Text.Json; -using ExoAuth.Infrastructure.Persistence; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace ExoAuth.Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(AppDbContext))] - [Migration("20260118174845_AddMagicLinkTokens")] - partial class AddMagicLinkTokens - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.11") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("ExoAuth.Domain.Entities.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("ApprovalAttempts") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) - .HasColumnName("approval_attempts"); - - b.Property("ApprovalCodeHash") - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("approval_code_hash"); - - b.Property("ApprovalExpiresAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("approval_expires_at"); - - b.Property("ApprovalTokenHash") - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("approval_token_hash"); - - b.Property("Browser") - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("browser"); - - b.Property("BrowserVersion") - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasColumnName("browser_version"); - - b.Property("City") - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("city"); - - b.Property("Country") - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("country"); - - b.Property("CountryCode") - .HasMaxLength(2) - .HasColumnType("character varying(2)") - .HasColumnName("country_code"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("DeviceId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("device_id"); - - b.Property("DeviceType") - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasColumnName("device_type"); - - b.Property("Fingerprint") - .HasMaxLength(500) - .HasColumnType("character varying(500)") - .HasColumnName("fingerprint"); - - b.Property("IpAddress") - .HasMaxLength(45) - .HasColumnType("character varying(45)") - .HasColumnName("ip_address"); - - b.Property("LastUsedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_used_at"); - - b.Property("Latitude") - .HasColumnType("double precision") - .HasColumnName("latitude"); - - b.Property("Longitude") - .HasColumnType("double precision") - .HasColumnName("longitude"); - - b.Property("Name") - .HasMaxLength(200) - .HasColumnType("character varying(200)") - .HasColumnName("name"); - - b.Property("OperatingSystem") - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("operating_system"); - - b.Property("OsVersion") - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasColumnName("os_version"); - - b.Property("RevokedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("revoked_at"); - - b.Property("RiskFactors") - .HasMaxLength(500) - .HasColumnType("character varying(500)") - .HasColumnName("risk_factors"); - - b.Property("RiskScore") - .HasColumnType("integer") - .HasColumnName("risk_score"); - - b.Property("Status") - .HasColumnType("integer") - .HasColumnName("status"); - - b.Property("TrustedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("trusted_at"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UserAgent") - .HasMaxLength(1000) - .HasColumnType("character varying(1000)") - .HasColumnName("user_agent"); - - b.Property("UserId") - .HasColumnType("uuid") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("p_k_devices"); - - b.HasIndex("ApprovalTokenHash") - .HasDatabaseName("i_x_devices_approval_token_hash"); - - b.HasIndex("DeviceId") - .HasDatabaseName("i_x_devices_device_id"); - - b.HasIndex("LastUsedAt") - .HasDatabaseName("i_x_devices_last_used_at"); - - b.HasIndex("Status") - .HasDatabaseName("i_x_devices_status"); - - b.HasIndex("UserId") - .HasDatabaseName("i_x_devices_user_id"); - - b.HasIndex("UserId", "DeviceId") - .IsUnique() - .HasDatabaseName("i_x_devices_user_id_device_id"); - - b.HasIndex("UserId", "Status") - .HasDatabaseName("i_x_devices_user_id_status"); - - b.ToTable("devices", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.EmailAnnouncement", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("CreatedByUserId") - .HasColumnType("uuid") - .HasColumnName("created_by_user_id"); - - b.Property("FailedCount") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) - .HasColumnName("failed_count"); - - b.Property("HtmlBody") - .IsRequired() - .HasColumnType("text") - .HasColumnName("html_body"); - - b.Property("PlainTextBody") - .HasColumnType("text") - .HasColumnName("plain_text_body"); - - b.Property("SentAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("sent_at"); - - b.Property("SentCount") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) - .HasColumnName("sent_count"); - - b.Property("Status") - .HasColumnType("integer") - .HasColumnName("status"); - - b.Property("Subject") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)") - .HasColumnName("subject"); - - b.Property("TargetPermission") - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("target_permission"); - - b.Property("TargetType") - .HasColumnType("integer") - .HasColumnName("target_type"); - - b.Property("TargetUserIds") - .HasColumnType("text") - .HasColumnName("target_user_ids"); - - b.Property("TotalRecipients") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) - .HasColumnName("total_recipients"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("p_k_email_announcements"); - - b.HasIndex("CreatedAt") - .HasDatabaseName("i_x_email_announcements_created_at"); - - b.HasIndex("CreatedByUserId") - .HasDatabaseName("i_x_email_announcements_created_by_user_id"); - - b.HasIndex("SentAt") - .HasDatabaseName("i_x_email_announcements_sent_at"); - - b.HasIndex("Status") - .HasDatabaseName("i_x_email_announcements_status"); - - b.ToTable("email_announcements", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.EmailConfiguration", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("AutoRetryDlq") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("auto_retry_dlq"); - - b.Property("BackoffMultiplier") - .ValueGeneratedOnAdd() - .HasColumnType("double precision") - .HasDefaultValue(2.0) - .HasColumnName("backoff_multiplier"); - - b.Property("CircuitBreakerFailureThreshold") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(5) - .HasColumnName("circuit_breaker_failure_threshold"); - - b.Property("CircuitBreakerOpenDurationMinutes") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(30) - .HasColumnName("circuit_breaker_open_duration_minutes"); - - b.Property("CircuitBreakerWindowMinutes") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(10) - .HasColumnName("circuit_breaker_window_minutes"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("DlqRetryIntervalHours") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(6) - .HasColumnName("dlq_retry_interval_hours"); - - b.Property("EmailsEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true) - .HasColumnName("emails_enabled"); - - b.Property("InitialRetryDelayMs") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(1000) - .HasColumnName("initial_retry_delay_ms"); - - b.Property("MaxRetriesPerProvider") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(3) - .HasColumnName("max_retries_per_provider"); - - b.Property("MaxRetryDelayMs") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(60000) - .HasColumnName("max_retry_delay_ms"); - - b.Property("TestMode") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("test_mode"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("p_k_email_configuration"); - - b.ToTable("email_configuration", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.EmailLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("AnnouncementId") - .HasColumnType("uuid") - .HasColumnName("announcement_id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("FailedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("failed_at"); - - b.Property("Language") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("character varying(10)") - .HasColumnName("language"); - - b.Property("LastError") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)") - .HasColumnName("last_error"); - - b.Property("MovedToDlqAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("moved_to_dlq_at"); - - b.Property("QueuedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("queued_at"); - - b.Property("RecipientEmail") - .IsRequired() - .HasMaxLength(320) - .HasColumnType("character varying(320)") - .HasColumnName("recipient_email"); - - b.Property("RecipientUserId") - .HasColumnType("uuid") - .HasColumnName("recipient_user_id"); - - b.Property("RetryCount") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) - .HasColumnName("retry_count"); - - b.Property("SentAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("sent_at"); - - b.Property("SentViaProviderId") - .HasColumnType("uuid") - .HasColumnName("sent_via_provider_id"); - - b.Property("Status") - .HasColumnType("integer") - .HasColumnName("status"); - - b.Property("Subject") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)") - .HasColumnName("subject"); - - b.Property("TemplateName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("template_name"); - - b.Property("TemplateVariables") - .HasColumnType("text") - .HasColumnName("template_variables"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("p_k_email_logs"); - - b.HasIndex("AnnouncementId") - .HasDatabaseName("i_x_email_logs_announcement_id"); - - b.HasIndex("QueuedAt") - .HasDatabaseName("i_x_email_logs_queued_at"); - - b.HasIndex("RecipientEmail") - .HasDatabaseName("i_x_email_logs_recipient_email"); - - b.HasIndex("RecipientUserId") - .HasDatabaseName("i_x_email_logs_recipient_user_id"); - - b.HasIndex("SentAt") - .HasDatabaseName("i_x_email_logs_sent_at"); - - b.HasIndex("SentViaProviderId") - .HasDatabaseName("i_x_email_logs_sent_via_provider_id"); - - b.HasIndex("Status") - .HasDatabaseName("i_x_email_logs_status"); - - b.HasIndex("TemplateName") - .HasDatabaseName("i_x_email_logs_template_name"); - - b.HasIndex("Status", "QueuedAt") - .HasDatabaseName("i_x_email_logs_status_queued_at"); - - b.ToTable("email_logs", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.EmailProvider", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("CircuitBreakerOpenUntil") - .HasColumnType("timestamp with time zone") - .HasColumnName("circuit_breaker_open_until"); - - b.Property("ConfigurationEncrypted") - .IsRequired() - .HasColumnType("text") - .HasColumnName("configuration_encrypted"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("FailureCount") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) - .HasColumnName("failure_count"); - - b.Property("IsEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true) - .HasColumnName("is_enabled"); - - b.Property("LastFailureAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_failure_at"); - - b.Property("LastSuccessAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_success_at"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)") - .HasColumnName("name"); - - b.Property("Priority") - .HasColumnType("integer") - .HasColumnName("priority"); - - b.Property("TotalFailed") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) - .HasColumnName("total_failed"); - - b.Property("TotalSent") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) - .HasColumnName("total_sent"); - - b.Property("Type") - .HasColumnType("integer") - .HasColumnName("type"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("p_k_email_providers"); - - b.HasIndex("IsEnabled") - .HasDatabaseName("i_x_email_providers_is_enabled"); - - b.HasIndex("Priority") - .HasDatabaseName("i_x_email_providers_priority"); - - b.HasIndex("Type") - .HasDatabaseName("i_x_email_providers_type"); - - b.HasIndex("IsEnabled", "Priority") - .HasDatabaseName("i_x_email_providers_is_enabled_priority"); - - b.ToTable("email_providers", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.IpRestriction", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("CreatedByUserId") - .HasColumnType("uuid") - .HasColumnName("created_by_user_id"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires_at"); - - b.Property("IpAddress") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasColumnName("ip_address"); - - b.Property("Reason") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)") - .HasColumnName("reason"); - - b.Property("Source") - .HasColumnType("integer") - .HasColumnName("source"); - - b.Property("Type") - .HasColumnType("integer") - .HasColumnName("type"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("p_k_ip_restrictions"); - - b.HasIndex("CreatedByUserId") - .HasDatabaseName("i_x_ip_restrictions_created_by_user_id"); - - b.HasIndex("ExpiresAt") - .HasDatabaseName("i_x_ip_restrictions_expires_at"); - - b.HasIndex("IpAddress") - .HasDatabaseName("i_x_ip_restrictions_ip_address"); - - b.HasIndex("Source") - .HasDatabaseName("i_x_ip_restrictions_source"); - - b.HasIndex("Type") - .HasDatabaseName("i_x_ip_restrictions_type"); - - b.HasIndex("Type", "ExpiresAt") - .HasDatabaseName("i_x_ip_restrictions_type_expires_at"); - - b.ToTable("ip_restrictions", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.LoginPattern", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("LastCity") - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("last_city"); - - b.Property("LastCountry") - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("last_country"); - - b.Property("LastIpAddress") - .HasMaxLength(45) - .HasColumnType("character varying(45)") - .HasColumnName("last_ip_address"); - - b.Property("LastLatitude") - .HasColumnType("double precision") - .HasColumnName("last_latitude"); - - b.Property("LastLoginAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_login_at"); - - b.Property("LastLongitude") - .HasColumnType("double precision") - .HasColumnName("last_longitude"); - - b.Property("TypicalCities") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(1000) - .HasColumnType("character varying(1000)") - .HasDefaultValue("[]") - .HasColumnName("typical_cities"); - - b.Property("TypicalCountries") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(500) - .HasColumnType("character varying(500)") - .HasDefaultValue("[]") - .HasColumnName("typical_countries"); - - b.Property("TypicalDeviceTypes") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(200) - .HasColumnType("character varying(200)") - .HasDefaultValue("[]") - .HasColumnName("typical_device_types"); - - b.Property("TypicalHours") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(200) - .HasColumnType("character varying(200)") - .HasDefaultValue("[]") - .HasColumnName("typical_hours"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UserId") - .HasColumnType("uuid") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("p_k_login_patterns"); - - b.HasIndex("UserId") - .IsUnique() - .HasDatabaseName("i_x_login_patterns_user_id"); - - b.ToTable("login_patterns", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.MagicLinkToken", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires_at"); - - b.Property("IsUsed") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_used"); - - b.Property("TokenHash") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)") - .HasColumnName("token_hash"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UsedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("used_at"); - - b.Property("UserId") - .HasColumnType("uuid") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("p_k_magic_link_tokens"); - - b.HasIndex("ExpiresAt") - .HasDatabaseName("i_x_magic_link_tokens_expires_at"); - - b.HasIndex("TokenHash") - .IsUnique() - .HasDatabaseName("i_x_magic_link_tokens_token_hash"); - - b.HasIndex("UserId") - .HasDatabaseName("i_x_magic_link_tokens_user_id"); - - b.HasIndex("UserId", "IsUsed", "ExpiresAt") - .HasDatabaseName("i_x_magic_link_tokens_user_id_is_used_expires_at"); - - b.ToTable("magic_link_tokens", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.MfaBackupCode", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("CodeHash") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("code_hash"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("IsUsed") - .HasColumnType("boolean") - .HasColumnName("is_used"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UsedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("used_at"); - - b.Property("UserId") - .HasColumnType("uuid") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("p_k_mfa_backup_codes"); - - b.HasIndex("UserId", "IsUsed") - .HasDatabaseName("i_x__mfa_backup_codes__user_id__is_used"); - - b.ToTable("mfa_backup_codes", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.Passkey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("AaGuid") - .HasColumnType("uuid") - .HasColumnName("aa_guid"); - - b.Property("Counter") - .HasColumnType("bigint") - .HasColumnName("counter"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("CredType") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)") - .HasColumnName("cred_type"); - - b.Property("CredentialId") - .IsRequired() - .HasMaxLength(1024) - .HasColumnType("bytea") - .HasColumnName("credential_id"); - - b.Property("LastUsedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_used_at"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("name"); - - b.Property("PublicKey") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("bytea") - .HasColumnName("public_key"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UserId") - .HasColumnType("uuid") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("p_k_passkeys"); - - b.HasIndex("CredentialId") - .IsUnique() - .HasDatabaseName("i_x__passkeys__credential_id"); - - b.HasIndex("UserId") - .HasDatabaseName("i_x__passkeys__user_id"); - - b.ToTable("passkeys", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.PasswordResetToken", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("CodeHash") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)") - .HasColumnName("code_hash"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires_at"); - - b.Property("IsUsed") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_used"); - - b.Property("TokenHash") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)") - .HasColumnName("token_hash"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UsedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("used_at"); - - b.Property("UserId") - .HasColumnType("uuid") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("p_k_password_reset_tokens"); - - b.HasIndex("CodeHash") - .HasDatabaseName("i_x_password_reset_tokens_code_hash"); - - b.HasIndex("ExpiresAt") - .HasDatabaseName("i_x_password_reset_tokens_expires_at"); - - b.HasIndex("TokenHash") - .IsUnique() - .HasDatabaseName("i_x_password_reset_tokens_token_hash"); - - b.HasIndex("UserId") - .HasDatabaseName("i_x_password_reset_tokens_user_id"); - - b.HasIndex("UserId", "IsUsed", "ExpiresAt") - .HasDatabaseName("i_x_password_reset_tokens_user_id_is_used_expires_at"); - - b.ToTable("password_reset_tokens", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.RefreshToken", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("DeviceId") - .HasColumnType("uuid") - .HasColumnName("device_id"); - - b.Property("DeviceInfo") - .HasMaxLength(500) - .HasColumnType("character varying(500)") - .HasColumnName("device_info"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires_at"); - - b.Property("IpAddress") - .HasMaxLength(45) - .HasColumnType("character varying(45)") - .HasColumnName("ip_address"); - - b.Property("IsRevoked") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_revoked"); - - b.Property("RememberMe") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("remember_me"); - - b.Property("RevokedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("revoked_at"); - - b.Property("SystemUserId") - .HasColumnType("uuid") - .HasColumnName("system_user_id"); - - b.Property("TokenHash") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)") - .HasColumnName("token_hash"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UserId") - .HasColumnType("uuid") - .HasColumnName("user_id"); - - b.Property("UserType") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)") - .HasColumnName("user_type"); - - b.HasKey("Id") - .HasName("p_k_refresh_tokens"); - - b.HasIndex("DeviceId") - .HasDatabaseName("i_x_refresh_tokens_device_id"); - - b.HasIndex("ExpiresAt") - .HasDatabaseName("i_x_refresh_tokens_expires_at"); - - b.HasIndex("IsRevoked") - .HasDatabaseName("i_x_refresh_tokens_is_revoked"); - - b.HasIndex("SystemUserId") - .HasDatabaseName("i_x_refresh_tokens_system_user_id"); - - b.HasIndex("TokenHash") - .HasDatabaseName("i_x_refresh_tokens_token_hash"); - - b.HasIndex("UserId") - .HasDatabaseName("i_x_refresh_tokens_user_id"); - - b.HasIndex("UserType") - .HasDatabaseName("i_x_refresh_tokens_user_type"); - - b.HasIndex("UserId", "UserType", "IsRevoked") - .HasDatabaseName("i_x_refresh_tokens_user_id_user_type_is_revoked"); - - b.ToTable("refresh_tokens", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.SystemAuditLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("Action") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("action"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("Details") - .HasColumnType("text") - .HasColumnName("details"); - - b.Property("EntityId") - .HasColumnType("uuid") - .HasColumnName("entity_id"); - - b.Property("EntityType") - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("entity_type"); - - b.Property("IpAddress") - .HasMaxLength(45) - .HasColumnType("character varying(45)") - .HasColumnName("ip_address"); - - b.Property("TargetUserId") - .HasColumnType("uuid") - .HasColumnName("target_user_id"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UserAgent") - .HasMaxLength(500) - .HasColumnType("character varying(500)") - .HasColumnName("user_agent"); - - b.Property("UserId") - .HasColumnType("uuid") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("p_k_system_audit_logs"); - - b.HasIndex("Action") - .HasDatabaseName("i_x_system_audit_logs_action"); - - b.HasIndex("CreatedAt") - .HasDatabaseName("i_x_system_audit_logs_created_at"); - - b.HasIndex("EntityType") - .HasDatabaseName("i_x_system_audit_logs_entity_type"); - - b.HasIndex("TargetUserId") - .HasDatabaseName("i_x_system_audit_logs_target_user_id"); - - b.HasIndex("UserId") - .HasDatabaseName("i_x_system_audit_logs_user_id"); - - b.HasIndex("Action", "CreatedAt") - .HasDatabaseName("i_x_system_audit_logs_action_created_at"); - - b.ToTable("system_audit_logs", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.SystemInvite", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("AcceptedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("accepted_at"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("email"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires_at"); - - b.Property("FirstName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("first_name"); - - b.Property("InvitedBy") - .HasColumnType("uuid") - .HasColumnName("invited_by"); - - b.Property("Language") - .IsRequired() - .HasColumnType("text") - .HasColumnName("language"); - - b.Property("LastName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("last_name"); - - b.Property("PermissionIds") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("permission_ids"); - - b.Property("ResentAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("resent_at"); - - b.Property("RevokedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("revoked_at"); - - b.Property("TokenHash") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)") - .HasColumnName("token_hash"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("p_k_system_invites"); - - b.HasIndex("AcceptedAt") - .HasDatabaseName("i_x_system_invites_accepted_at"); - - b.HasIndex("CreatedAt") - .HasDatabaseName("i_x_system_invites_created_at"); - - b.HasIndex("Email") - .HasDatabaseName("i_x_system_invites_email"); - - b.HasIndex("ExpiresAt") - .HasDatabaseName("i_x_system_invites_expires_at"); - - b.HasIndex("InvitedBy") - .HasDatabaseName("i_x_system_invites_invited_by"); - - b.HasIndex("RevokedAt") - .HasDatabaseName("i_x_system_invites_revoked_at"); - - b.HasIndex("TokenHash") - .IsUnique() - .HasDatabaseName("i_x_system_invites_token_hash"); - - b.ToTable("system_invites", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.SystemPermission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("Category") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)") - .HasColumnName("category"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("Description") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)") - .HasColumnName("description"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("name"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("p_k_system_permissions"); - - b.HasIndex("Category") - .HasDatabaseName("i_x_system_permissions_category"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("i_x_system_permissions_name"); - - b.ToTable("system_permissions", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.SystemUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("AnonymizedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("anonymized_at"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("email"); - - b.Property("EmailVerified") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("email_verified"); - - b.Property("FailedLoginAttempts") - .HasColumnType("integer") - .HasColumnName("failed_login_attempts"); - - b.Property("FirstName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("first_name"); - - b.Property("IsActive") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true) - .HasColumnName("is_active"); - - b.Property("IsAnonymized") - .HasColumnType("boolean") - .HasColumnName("is_anonymized"); - - b.Property("LastLoginAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_login_at"); - - b.Property("LastName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)") - .HasColumnName("last_name"); - - b.Property("LockedUntil") - .HasColumnType("timestamp with time zone") - .HasColumnName("locked_until"); - - b.Property("MfaEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("mfa_enabled"); - - b.Property("MfaEnabledAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("mfa_enabled_at"); - - b.Property("MfaSecret") - .HasMaxLength(500) - .HasColumnType("character varying(500)") - .HasColumnName("mfa_secret"); - - b.Property("PasswordHash") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("password_hash"); - - b.Property("PreferredLanguage") - .IsRequired() - .ValueGeneratedOnAdd() - .HasMaxLength(10) - .HasColumnType("character varying(10)") - .HasDefaultValue("en") - .HasColumnName("preferred_language"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("p_k_system_users"); - - b.HasIndex("Email") - .IsUnique() - .HasDatabaseName("i_x_system_users_email"); - - b.HasIndex("IsActive") - .HasDatabaseName("i_x_system_users_is_active"); - - b.HasIndex("IsAnonymized") - .HasDatabaseName("i_x_system_users_is_anonymized"); - - b.HasIndex("LockedUntil") - .HasDatabaseName("i_x_system_users_locked_until"); - - b.HasIndex("MfaEnabled") - .HasDatabaseName("i_x_system_users_mfa_enabled"); - - b.ToTable("system_users", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.SystemUserPermission", b => - { - b.Property("SystemUserId") - .HasColumnType("uuid") - .HasColumnName("system_user_id"); - - b.Property("SystemPermissionId") - .HasColumnType("uuid") - .HasColumnName("system_permission_id"); - - b.Property("GrantedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("granted_at"); - - b.Property("GrantedBy") - .HasColumnType("uuid") - .HasColumnName("granted_by"); - - b.HasKey("SystemUserId", "SystemPermissionId") - .HasName("p_k_system_user_permissions"); - - b.HasIndex("SystemPermissionId") - .HasDatabaseName("i_x_system_user_permissions_system_permission_id"); - - b.HasIndex("SystemUserId") - .HasDatabaseName("i_x_system_user_permissions_system_user_id"); - - b.ToTable("system_user_permissions", (string)null); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.Device", b => - { - b.HasOne("ExoAuth.Domain.Entities.SystemUser", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("f_k_devices_system_users_user_id"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.EmailAnnouncement", b => - { - b.HasOne("ExoAuth.Domain.Entities.SystemUser", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedByUserId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired() - .HasConstraintName("f_k_email_announcements_system_users_created_by_user_id"); - - b.Navigation("CreatedByUser"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.EmailLog", b => - { - b.HasOne("ExoAuth.Domain.Entities.EmailAnnouncement", "Announcement") - .WithMany("EmailLogs") - .HasForeignKey("AnnouncementId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("f_k_email_logs_email_announcements_announcement_id"); - - b.HasOne("ExoAuth.Domain.Entities.SystemUser", "RecipientUser") - .WithMany() - .HasForeignKey("RecipientUserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("f_k_email_logs_system_users_recipient_user_id"); - - b.HasOne("ExoAuth.Domain.Entities.EmailProvider", "SentViaProvider") - .WithMany() - .HasForeignKey("SentViaProviderId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("f_k_email_logs_email_providers_sent_via_provider_id"); - - b.Navigation("Announcement"); - - b.Navigation("RecipientUser"); - - b.Navigation("SentViaProvider"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.IpRestriction", b => - { - b.HasOne("ExoAuth.Domain.Entities.SystemUser", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedByUserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("f_k_ip_restrictions_system_users_created_by_user_id"); - - b.Navigation("CreatedByUser"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.LoginPattern", b => - { - b.HasOne("ExoAuth.Domain.Entities.SystemUser", "User") - .WithOne() - .HasForeignKey("ExoAuth.Domain.Entities.LoginPattern", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("f_k_login_patterns_system_users_user_id"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.MagicLinkToken", b => - { - b.HasOne("ExoAuth.Domain.Entities.SystemUser", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("f_k_magic_link_tokens_system_users_user_id"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.MfaBackupCode", b => - { - b.HasOne("ExoAuth.Domain.Entities.SystemUser", "User") - .WithMany("MfaBackupCodes") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("f_k_mfa_backup_codes_system_users_user_id"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.Passkey", b => - { - b.HasOne("ExoAuth.Domain.Entities.SystemUser", "User") - .WithMany("Passkeys") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("f_k_passkeys_system_users_user_id"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.PasswordResetToken", b => - { - b.HasOne("ExoAuth.Domain.Entities.SystemUser", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("f_k_password_reset_tokens_system_users_user_id"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.RefreshToken", b => - { - b.HasOne("ExoAuth.Domain.Entities.Device", "Device") - .WithMany("RefreshTokens") - .HasForeignKey("DeviceId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("f_k_refresh_tokens_devices_device_id"); - - b.HasOne("ExoAuth.Domain.Entities.SystemUser", null) - .WithMany("RefreshTokens") - .HasForeignKey("SystemUserId") - .HasConstraintName("f_k_refresh_tokens_system_users_system_user_id"); - - b.Navigation("Device"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.SystemAuditLog", b => - { - b.HasOne("ExoAuth.Domain.Entities.SystemUser", "TargetUser") - .WithMany() - .HasForeignKey("TargetUserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("f_k_system_audit_logs_system_users_target_user_id"); - - b.HasOne("ExoAuth.Domain.Entities.SystemUser", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("f_k_system_audit_logs_system_users_user_id"); - - b.Navigation("TargetUser"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.SystemInvite", b => - { - b.HasOne("ExoAuth.Domain.Entities.SystemUser", "InvitedByUser") - .WithMany() - .HasForeignKey("InvitedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired() - .HasConstraintName("f_k_system_invites_system_users_invited_by"); - - b.Navigation("InvitedByUser"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.SystemUserPermission", b => - { - b.HasOne("ExoAuth.Domain.Entities.SystemPermission", "SystemPermission") - .WithMany("UserPermissions") - .HasForeignKey("SystemPermissionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("f_k_system_user_permissions_system_permissions_system_permissio~"); - - b.HasOne("ExoAuth.Domain.Entities.SystemUser", "SystemUser") - .WithMany("Permissions") - .HasForeignKey("SystemUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("f_k_system_user_permissions_system_users_system_user_id"); - - b.Navigation("SystemPermission"); - - b.Navigation("SystemUser"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.Device", b => - { - b.Navigation("RefreshTokens"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.EmailAnnouncement", b => - { - b.Navigation("EmailLogs"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.SystemPermission", b => - { - b.Navigation("UserPermissions"); - }); - - modelBuilder.Entity("ExoAuth.Domain.Entities.SystemUser", b => - { - b.Navigation("MfaBackupCodes"); - - b.Navigation("Passkeys"); - - b.Navigation("Permissions"); - - b.Navigation("RefreshTokens"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/src/ExoAuth.Infrastructure/Persistence/Migrations/20260118174845_AddMagicLinkTokens.cs b/backend/src/ExoAuth.Infrastructure/Persistence/Migrations/20260118174845_AddMagicLinkTokens.cs deleted file mode 100644 index 5666e0e..0000000 --- a/backend/src/ExoAuth.Infrastructure/Persistence/Migrations/20260118174845_AddMagicLinkTokens.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace ExoAuth.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddMagicLinkTokens : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "magic_link_tokens", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - user_id = table.Column(type: "uuid", nullable: false), - token_hash = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - expires_at = table.Column(type: "timestamp with time zone", nullable: false), - is_used = table.Column(type: "boolean", nullable: false, defaultValue: false), - used_at = table.Column(type: "timestamp with time zone", nullable: true), - created_at = table.Column(type: "timestamp with time zone", nullable: false), - updated_at = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("p_k_magic_link_tokens", x => x.id); - table.ForeignKey( - name: "f_k_magic_link_tokens_system_users_user_id", - column: x => x.user_id, - principalTable: "system_users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "i_x_magic_link_tokens_expires_at", - table: "magic_link_tokens", - column: "expires_at"); - - migrationBuilder.CreateIndex( - name: "i_x_magic_link_tokens_token_hash", - table: "magic_link_tokens", - column: "token_hash", - unique: true); - - migrationBuilder.CreateIndex( - name: "i_x_magic_link_tokens_user_id", - table: "magic_link_tokens", - column: "user_id"); - - migrationBuilder.CreateIndex( - name: "i_x_magic_link_tokens_user_id_is_used_expires_at", - table: "magic_link_tokens", - columns: new[] { "user_id", "is_used", "expires_at" }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "magic_link_tokens"); - } - } -} diff --git a/backend/src/ExoAuth.Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs b/backend/src/ExoAuth.Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs index a9c0db0..6b95556 100644 --- a/backend/src/ExoAuth.Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs +++ b/backend/src/ExoAuth.Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs @@ -729,64 +729,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("login_patterns", (string)null); }); - modelBuilder.Entity("ExoAuth.Domain.Entities.MagicLinkToken", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires_at"); - - b.Property("IsUsed") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_used"); - - b.Property("TokenHash") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)") - .HasColumnName("token_hash"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UsedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("used_at"); - - b.Property("UserId") - .HasColumnType("uuid") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("p_k_magic_link_tokens"); - - b.HasIndex("ExpiresAt") - .HasDatabaseName("i_x_magic_link_tokens_expires_at"); - - b.HasIndex("TokenHash") - .IsUnique() - .HasDatabaseName("i_x_magic_link_tokens_token_hash"); - - b.HasIndex("UserId") - .HasDatabaseName("i_x_magic_link_tokens_user_id"); - - b.HasIndex("UserId", "IsUsed", "ExpiresAt") - .HasDatabaseName("i_x_magic_link_tokens_user_id_is_used_expires_at"); - - b.ToTable("magic_link_tokens", (string)null); - }); - modelBuilder.Entity("ExoAuth.Domain.Entities.MfaBackupCode", b => { b.Property("Id") @@ -1502,18 +1444,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("ExoAuth.Domain.Entities.MagicLinkToken", b => - { - b.HasOne("ExoAuth.Domain.Entities.SystemUser", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("f_k_magic_link_tokens_system_users_user_id"); - - b.Navigation("User"); - }); - modelBuilder.Entity("ExoAuth.Domain.Entities.MfaBackupCode", b => { b.HasOne("ExoAuth.Domain.Entities.SystemUser", "User") diff --git a/backend/src/ExoAuth.Infrastructure/Services/EmailService.cs b/backend/src/ExoAuth.Infrastructure/Services/EmailService.cs index ea36491..9e59caa 100644 --- a/backend/src/ExoAuth.Infrastructure/Services/EmailService.cs +++ b/backend/src/ExoAuth.Infrastructure/Services/EmailService.cs @@ -14,7 +14,6 @@ public sealed class EmailService : IEmailService private readonly int _inviteExpirationHours; private readonly int _passwordResetExpiryMinutes; private readonly int _deviceApprovalExpiryMinutes; - private readonly int _magicLinkExpiryMinutes; public EmailService( IMessageBus messageBus, @@ -32,7 +31,6 @@ public EmailService( _passwordResetExpiryMinutes = configuration.GetValue("Auth:PasswordResetExpiryMinutes", 15); _deviceApprovalExpiryMinutes = configuration.GetValue("DeviceTrust:ApprovalExpiryMinutes", 30); - _magicLinkExpiryMinutes = configuration.GetValue("Auth:MagicLinkExpiryMinutes", 15); } public async Task SendAsync( @@ -118,35 +116,6 @@ await SendAsync( ); } - public async Task SendMagicLinkAsync( - string email, - string firstName, - string magicLinkToken, - Guid userId, - string language = "en-US", - CancellationToken cancellationToken = default) - { - var magicLinkUrl = $"{_baseUrl}/magic-link-login?token={magicLinkToken}"; - - var variables = new Dictionary - { - ["firstName"] = firstName, - ["magicLinkUrl"] = magicLinkUrl, - ["expirationMinutes"] = _magicLinkExpiryMinutes.ToString(), - ["year"] = DateTime.UtcNow.Year.ToString() - }; - - await SendAsync( - to: email, - subject: _templateService.GetSubject("magic-link", language), - templateName: "magic-link", - variables: variables, - language: language, - recipientUserId: userId, - cancellationToken: cancellationToken - ); - } - public async Task SendPasswordChangedAsync( string email, string firstName, diff --git a/backend/src/ExoAuth.Infrastructure/Services/MagicLinkService.cs b/backend/src/ExoAuth.Infrastructure/Services/MagicLinkService.cs deleted file mode 100644 index ed60365..0000000 --- a/backend/src/ExoAuth.Infrastructure/Services/MagicLinkService.cs +++ /dev/null @@ -1,121 +0,0 @@ -using ExoAuth.Application.Common.Interfaces; -using ExoAuth.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace ExoAuth.Infrastructure.Services; - -public sealed class MagicLinkService : IMagicLinkService -{ - private readonly IAppDbContext _context; - private readonly ILogger _logger; - private readonly int _expirationMinutes; - private const int MaxRetries = 3; - - public MagicLinkService( - IAppDbContext context, - IConfiguration configuration, - ILogger logger) - { - _context = context; - _logger = logger; - _expirationMinutes = configuration.GetValue("Auth:MagicLinkExpiryMinutes", 15); - } - - public async Task CreateMagicLinkAsync(Guid userId, CancellationToken cancellationToken = default) - { - // Invalidate any existing tokens for this user first - await InvalidateAllTokensAsync(userId, cancellationToken); - - // Generate with collision prevention - for (var attempt = 0; attempt < MaxRetries; attempt++) - { - var token = MagicLinkToken.GenerateToken(); - - // Check for token hash collision (extremely unlikely but we check anyway) - var tokenHash = HashForCheck(token); - var exists = await _context.MagicLinkTokens - .AnyAsync(x => x.TokenHash == tokenHash, cancellationToken); - - if (exists) - { - _logger.LogWarning("Token collision detected on attempt {Attempt}, regenerating", attempt + 1); - continue; - } - - var entity = MagicLinkToken.Create(userId, token, _expirationMinutes); - - await _context.MagicLinkTokens.AddAsync(entity, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - - _logger.LogInformation("Magic link token created for user {UserId}", userId); - - return new MagicLinkResult(entity, token); - } - - // This should never happen given the entropy of our tokens - throw new InvalidOperationException("Failed to generate unique token after maximum retries"); - } - - public async Task ValidateTokenAsync(string token, CancellationToken cancellationToken = default) - { - var tokenHash = HashForCheck(token); - - var magicLinkToken = await _context.MagicLinkTokens - .Include(x => x.User) - .FirstOrDefaultAsync(x => x.TokenHash == tokenHash, cancellationToken); - - if (magicLinkToken is null) - { - _logger.LogDebug("Magic link token not found"); - return null; - } - - if (!magicLinkToken.IsValid) - { - _logger.LogDebug("Magic link token is invalid (used: {IsUsed}, expired: {IsExpired})", - magicLinkToken.IsUsed, magicLinkToken.IsExpired); - return null; - } - - return magicLinkToken; - } - - public async Task MarkAsUsedAsync(MagicLinkToken token, CancellationToken cancellationToken = default) - { - token.MarkAsUsed(); - await _context.SaveChangesAsync(cancellationToken); - - _logger.LogInformation("Magic link token marked as used for user {UserId}", token.UserId); - } - - public async Task InvalidateAllTokensAsync(Guid userId, CancellationToken cancellationToken = default) - { - var pendingTokens = await _context.MagicLinkTokens - .Where(x => x.UserId == userId && !x.IsUsed) - .ToListAsync(cancellationToken); - - foreach (var token in pendingTokens) - { - token.MarkAsUsed(); - } - - if (pendingTokens.Count > 0) - { - await _context.SaveChangesAsync(cancellationToken); - _logger.LogInformation("Invalidated {Count} pending magic link tokens for user {UserId}", - pendingTokens.Count, userId); - } - } - - /// - /// Hash a token for lookup comparison. - /// Must match the hashing in MagicLinkToken entity. - /// - private static string HashForCheck(string token) - { - var bytes = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(token)); - return Convert.ToBase64String(bytes); - } -} diff --git a/backend/swagger-verification.md b/backend/swagger-verification.md deleted file mode 100644 index f9eb575..0000000 --- a/backend/swagger-verification.md +++ /dev/null @@ -1,90 +0,0 @@ -# Swagger/OpenAPI Documentation Verification - -## Changes Made - -### 1. Enabled XML Documentation Generation -**File:** `backend/src/ExoAuth.Api/ExoAuth.Api.csproj` - -Added XML documentation generation properties: -```xml -true -$(NoWarn);1591 -``` - -### 2. Configured Swagger to Include XML Comments -**File:** `backend/src/ExoAuth.Api/Extensions/ServiceCollectionExtensions.cs` - -Updated `AddSwaggerConfiguration` to load XML documentation: -```csharp -// Include XML documentation -var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; -var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); -options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true); -``` - -## Magic Link Endpoints Documentation - -The following endpoints are now documented in Swagger/OpenAPI: - -### POST /api/system/auth/magic-link/request -**Summary:** Request a magic link email for passwordless login. - -**Request Body:** -- `email` (string): User's email address -- `captchaToken` (string, optional): CAPTCHA token for anti-abuse - -**Response:** `RequestMagicLinkResponse` (200 OK) - -### POST /api/system/auth/magic-link/login -**Summary:** Login with magic link token. - -**Request Body:** -- `token` (string): Magic link token from email -- `deviceId` (string, optional): Device identifier -- `deviceFingerprint` (string, optional): Device fingerprint for risk scoring -- `rememberMe` (boolean): Whether to extend session duration - -**Response:** `AuthResponse` (200 OK) -- May include `mfaRequired`, `mfaSetupRequired`, or `deviceApprovalRequired` flags -- Returns access and refresh tokens when authentication is complete - -## Verification Steps - -1. **Build the project:** - ```bash - cd backend/src/ExoAuth.Api - dotnet build - ``` - -2. **Verify XML file generation:** - ```bash - ls -la bin/Debug/net8.0/ExoAuth.Api.xml - ``` - -3. **Start the API:** - ```bash - dotnet run --project backend/src/ExoAuth.Api - ``` - -4. **Access Swagger UI:** - Open browser to: http://localhost:5096/swagger - -5. **Verify endpoints appear with descriptions:** - - Navigate to the "Auth" section - - Expand "POST /api/system/auth/magic-link/request" - - Expand "POST /api/system/auth/magic-link/login" - - Verify that descriptions are visible - -6. **Verify JSON schema:** - ```bash - curl -s http://localhost:5096/swagger/v1/swagger.json | jq '.paths | keys | map(select(. | contains("magic-link")))' - ``` - -## Expected Result - -Both magic link endpoints should appear in the Swagger documentation with: -- ✅ Proper request/response schemas -- ✅ XML comment descriptions -- ✅ Parameter documentation -- ✅ Response type definitions -- ✅ Status code documentation (200, 400, 401, 429) diff --git a/backend/templates/emails/de-DE/magic-link.html b/backend/templates/emails/de-DE/magic-link.html deleted file mode 100644 index 4d6d332..0000000 --- a/backend/templates/emails/de-DE/magic-link.html +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - Bei deinem Konto anmelden - - - - - - -
- - - - - - - - - - - - - - - -
-
- ExoAuth -
-

Bei deinem Konto anmelden

-
-

Hallo {{firstName}},

- -

- Klicke auf den Button unten, um dich bei deinem Konto anzumelden. Kein Passwort erforderlich! -

- - - - - - -
- - Anmelden - -
- - - - - - -
-

- ⏰ Dieser Link ist {{expirationMinutes}} Minuten gültig. -

-
- -

- Wenn du diesen Anmelde-Link nicht angefordert hast, kannst du diese E-Mail ignorieren. -

- - -

- Falls der Button nicht funktioniert, kopiere diesen Link in deinen Browser:
- {{magicLinkUrl}} -

-
-

- © {{year}} ExoAuth. Alle Rechte vorbehalten. -

-
-
- - diff --git a/backend/templates/emails/de-DE/subjects.json b/backend/templates/emails/de-DE/subjects.json index f1136b2..1458d0d 100644 --- a/backend/templates/emails/de-DE/subjects.json +++ b/backend/templates/emails/de-DE/subjects.json @@ -14,6 +14,5 @@ "device-approval-required": "Gerätebestätigung erforderlich", "device-denied-alert": "Sicherheitswarnung: Gerätezugriff verweigert", "passkey-registered": "Passkey zu Ihrem Konto hinzugefügt", - "passkey-removed": "Passkey aus Ihrem Konto entfernt", - "magic-link": "Ihr Magic Link zum Anmelden" + "passkey-removed": "Passkey aus Ihrem Konto entfernt" } diff --git a/backend/templates/emails/en-US/magic-link.html b/backend/templates/emails/en-US/magic-link.html deleted file mode 100644 index d97fd81..0000000 --- a/backend/templates/emails/en-US/magic-link.html +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - Sign In to Your Account - - - - - - -
- - - - - - - - - - - - - - - -
-
- ExoAuth -
-

Sign In to Your Account

-
-

Hello {{firstName}},

- -

- Click the button below to sign in to your account. No password needed! -

- - - - - - -
- - Sign In - -
- - - - - - -
-

- ⏰ This link will expire in {{expirationMinutes}} minutes. -

-
- -

- If you didn't request this sign-in link, you can safely ignore this email. -

- - -

- If the button doesn't work, copy and paste this link into your browser:
- {{magicLinkUrl}} -

-
-

- © {{year}} ExoAuth. All rights reserved. -

-
-
- - diff --git a/backend/templates/emails/en-US/subjects.json b/backend/templates/emails/en-US/subjects.json index 97301fd..310b2cd 100644 --- a/backend/templates/emails/en-US/subjects.json +++ b/backend/templates/emails/en-US/subjects.json @@ -14,6 +14,5 @@ "device-approval-required": "Device Approval Required", "device-denied-alert": "Security Alert: Device Access Denied", "passkey-registered": "Passkey Registered to Your Account", - "passkey-removed": "Passkey Removed from Your Account", - "magic-link": "Your magic link to sign in" + "passkey-removed": "Passkey Removed from Your Account" } diff --git a/backend/test-magic-link.md b/backend/test-magic-link.md deleted file mode 100644 index 4317bfa..0000000 --- a/backend/test-magic-link.md +++ /dev/null @@ -1,213 +0,0 @@ -# Magic Link End-to-End Test Verification - -## Test Environment -- Backend API: http://localhost:5096 -- Frontend: http://localhost:5176 -- Database: PostgreSQL (exoauth database) -- Email: MailHog (localhost:1025/8025) -- RabbitMQ: localhost:5672/15672 - -## Test Results - -### ✅ 1. Database Schema -- [x] magic_link_tokens table exists -- [x] Columns: id, user_id, token_hash, expires_at, is_used, used_at, created_at, updated_at -- [x] Foreign key to system_users with cascade delete -- [x] Unique index on token_hash -- [x] Composite index on (user_id, is_used, expires_at) - -```sql -SELECT COUNT(*) FROM magic_link_tokens; --- Result: 3 tokens created during testing -``` - -### ✅ 2. Request Magic Link API Endpoint -**Endpoint:** POST /api/system/auth/magic-link/request - -**Test Request:** -```bash -curl -X POST http://localhost:5096/api/system/auth/magic-link/request \ - -H "Content-Type: application/json" \ - -H "X-Forwarded-For: 127.0.0.1" \ - -d '{"email":"test@example.com","captchaToken":"test-token"}' -``` - -**Response:** -```json -{ - "status": "success", - "statusCode": 200, - "message": "OK", - "data": { - "success": true, - "message": "If an account exists with this email, you will receive a magic link." - } -} -``` - -**Verification:** -- [x] HTTP 200 response -- [x] Anti-enumeration message returned -- [x] Token created in database (verified via SQL query) -- [x] Previous tokens invalidated -- [x] Audit log created (system.magic_link.requested) -- [x] Email queued to RabbitMQ - -**API Logs:** -``` -[20:01:20 INF] Invalidated 1 pending magic link tokens for user 9740cb72-5e08-4716-8042-8e388ebc3333 -[20:01:20 INF] Magic link token created for user 9740cb72-5e08-4716-8042-8e388ebc3333 -[20:01:20 INF] Queued email to test@example.com with template magic-link -[20:01:20 INF] Magic link email sent to test@example.com -``` - -### ✅ 3. Magic Link Token Generation -**Service:** MagicLinkService - -**Features Verified:** -- [x] Cryptographically secure token generation (32 bytes) -- [x] SHA256 token hashing -- [x] Collision prevention (3 retries) -- [x] 15-minute expiration (configurable) -- [x] Previous tokens invalidated on new request - -**Database Record:** -``` -id: c2ab2502-9296-401f-aba6-1456faf7a51c -user_id: 9740cb72-5e08-4716-8042-8e388ebc3333 -token_hash: 9SAmlnS8j8hwxkp/CZDxEbhMM5yKfx1UEwbGa3TaTO8= -expires_at: 2026-01-18 19:16:20.853079+00 -is_used: false -created_at: 2026-01-18 19:01:20.85305+00 -``` - -### ✅ 4. Email Templates -**Templates Created:** -- [x] backend/templates/emails/en-US/magic-link.html -- [x] backend/templates/emails/de-DE/magic-link.html - -**Template Variables:** -- {{firstName}} -- {{magicLinkUrl}} -- {{expirationMinutes}} -- {{year}} - -**Email Subjects:** -- en-US: "Your magic link to sign in" -- de-DE: "Ihr Magic Link zum Anmelden" - -### ⚠️ 5. Email Worker -**Status:** Email sending has configuration issue (database connection) - -**What Works:** -- [x] RabbitMQ connection established -- [x] SendEmailConsumer listening on queue -- [x] Email messages received from queue -- [x] Templates loaded correctly - -**Issue:** -- Email worker service is unable to connect to database for email configuration -- This prevents actual SMTP email sending -- Error: "The ConnectionString property has not been initialized" - -**Note:** This is a configuration/deployment issue, not a magic link feature issue. The API correctly creates tokens and queues emails. In production, this would be resolved with proper environment configuration. - -### ✅ 6. Frontend Components -**Components Created:** -- [x] MagicLinkForm (frontend/src/features/auth/components/magic-link-form.tsx) -- [x] MagicLinkSent (frontend/src/features/auth/components/magic-link-sent.tsx) - -**API Functions:** -- [x] requestMagicLink (auth-api.ts) -- [x] magicLinkLogin (auth-api.ts) - -**React Hooks:** -- [x] useRequestMagicLink -- [x] useMagicLinkLogin - -**Translations:** -- [x] English (en) translations -- [x] German (de) translations - -### ✅ 7. Frontend Routes -**Routes Created:** -- [x] /magic-link-login (token validation and auto-login) - -**Login Page Integration:** -- [x] "Sign in with magic link" toggle button -- [x] Mode switching between password and magic link -- [x] MagicLinkForm integration - -**Frontend Running:** http://localhost:5176 - -### ✅ 8. Security Features -**Implemented:** -- [x] CAPTCHA integration (configurable) -- [x] Rate limiting on request endpoint -- [x] Anti-enumeration (same response for existing/non-existing emails) -- [x] Token hashing (SHA256) -- [x] Single-use tokens -- [x] Time-limited tokens (15 minutes) -- [x] Automatic invalidation of previous tokens -- [x] Audit logging - -## Manual Test Flow Verification - -### Test User Created: -```sql -email: test@example.com -id: 9740cb72-5e08-4716-8042-8e388ebc3333 -email_verified: true -``` - -### Verification Steps Completed: - -1. ✅ Start backend API (dotnet run) - Running on port 5096 -2. ✅ Start email worker (dotnet run) - Running with RabbitMQ connection -3. ✅ Start frontend (npm run dev) - Running on port 5176 -4. ✅ Navigate to /login in browser - Frontend accessible -5. ✅ Click 'Sign in with magic link' - UI component exists -6. ✅ Enter valid email address - Form validation works -7. ✅ Submit form (verify 200 OK response) - **VERIFIED: HTTP 200** -8. ⚠️ Check email logs for magic link email - Email worker has DB config issue -9. ⚠️ Extract token from email - Cannot extract due to email sending issue -10. ⏸️ Navigate to /magic-link-login?token={token} - Pending token extraction -11. ⏸️ Verify redirect to /dashboard - Pending token -12. ⏸️ Verify user is authenticated - Pending token -13. ⏸️ Test expired token (show error) - Pending -14. ⏸️ Test reused token (show error) - Pending -15. ⏸️ Test rate limiting (too many requests) - Pending - -## Summary - -**Feature Implementation:** ✅ COMPLETE - -All magic link functionality has been implemented: -- Domain layer (MagicLinkToken entity) -- Service layer (IMagicLinkService, MagicLinkService) -- CQRS commands (RequestMagicLink, LoginWithMagicLink) -- Email templates (EN/DE) -- API endpoints (request, login) -- Frontend components and routes -- Security features (rate limiting, CAPTCHA, audit logging) - -**End-to-End Testing:** ⚠️ PARTIAL - -- API endpoints functional and tested -- Token generation and storage verified -- Frontend components built and accessible -- Email worker has deployment/configuration issue preventing SMTP sending - -**Recommended Next Steps:** -1. Fix email worker database connection configuration -2. Complete end-to-end test with actual email -3. Test expired token handling -4. Test reused token handling -5. Test rate limiting -6. Browser-based UI verification - -**Deployment Note:** -The email configuration issue is environment-specific. The code is correct and would work in a properly configured production environment with: -- Correct database connection string in email worker appsettings -- SMTP server configuration -- Data protection keys shared between API and worker diff --git a/backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/LoginWithMagicLinkHandlerTests.cs b/backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/LoginWithMagicLinkHandlerTests.cs deleted file mode 100644 index 916cd4e..0000000 --- a/backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/LoginWithMagicLinkHandlerTests.cs +++ /dev/null @@ -1,500 +0,0 @@ -using ExoAuth.Application.Common.Exceptions; -using ExoAuth.Application.Common.Interfaces; -using ExoAuth.Application.Common.Models; -using ExoAuth.Application.Features.Auth.Commands.LoginWithMagicLink; -using ExoAuth.Domain.Entities; -using ExoAuth.Domain.Enums; -using ExoAuth.UnitTests.Helpers; -using FluentAssertions; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Moq; - -namespace ExoAuth.UnitTests.Features.Auth.MagicLink; - -public sealed class LoginWithMagicLinkHandlerTests -{ - private readonly Mock _mockContext; - private readonly Mock _mockMagicLinkService; - private readonly Mock _mockUserRepository; - private readonly Mock _mockTokenService; - private readonly Mock _mockPermissionCache; - private readonly Mock _mockForceReauthService; - private readonly Mock _mockRevokedSessionService; - private readonly Mock _mockDeviceService; - private readonly Mock _mockAuditService; - private readonly Mock _mockMfaService; - private readonly Mock _mockEmailService; - private readonly Mock _mockEmailTemplateService; - private readonly Mock _mockConfiguration; - private readonly Mock _mockRiskScoringService; - private readonly Mock _mockLoginPatternService; - private readonly Mock _mockGeoIpService; - private readonly Mock _mockDeviceDetectionService; - private readonly Mock> _mockLogger; - private readonly LoginWithMagicLinkHandler _handler; - - public LoginWithMagicLinkHandlerTests() - { - _mockContext = MockDbContext.Create(); - _mockMagicLinkService = new Mock(); - _mockUserRepository = new Mock(); - _mockTokenService = new Mock(); - _mockPermissionCache = new Mock(); - _mockForceReauthService = new Mock(); - _mockRevokedSessionService = new Mock(); - _mockDeviceService = new Mock(); - _mockAuditService = new Mock(); - _mockMfaService = new Mock(); - _mockEmailService = new Mock(); - _mockEmailTemplateService = new Mock(); - _mockConfiguration = new Mock(); - _mockRiskScoringService = new Mock(); - _mockLoginPatternService = new Mock(); - _mockGeoIpService = new Mock(); - _mockDeviceDetectionService = new Mock(); - _mockLogger = new Mock>(); - - // Default token service setup - _mockTokenService.Setup(x => x.RefreshTokenExpiration).Returns(TimeSpan.FromDays(30)); - _mockTokenService.Setup(x => x.RememberMeExpirationDays).Returns(90); - _mockTokenService.Setup(x => x.GenerateAccessToken( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Returns("test-access-token"); - _mockTokenService.Setup(x => x.GenerateRefreshToken()) - .Returns("test-refresh-token"); - - // Default configuration setup - _mockConfiguration.Setup(x => x["SystemInvite:BaseUrl"]).Returns("http://localhost:5173"); - - // Default email template service setup - _mockEmailTemplateService.Setup(x => x.GetSubject(It.IsAny(), It.IsAny())) - .Returns((string template, string lang) => $"Subject for {template}"); - - // Default device service setup - return a trusted device by default - var mockDevice = TestDataFactory.CreateDevice(Guid.NewGuid()); - _mockDeviceService.Setup(x => x.GenerateDeviceId()).Returns("test-device-id"); - _mockDeviceService.Setup(x => x.FindTrustedDeviceAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(mockDevice); - - // Default geo IP and device detection setup - _mockGeoIpService.Setup(x => x.GetLocation(It.IsAny())) - .Returns(GeoLocation.Empty); - _mockDeviceDetectionService.Setup(x => x.Parse(It.IsAny())) - .Returns(DeviceInfo.Empty); - - // Default risk scoring setup - not suspicious - _mockRiskScoringService.Setup(x => x.CalculateAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(RiskScore.Low()); - _mockRiskScoringService.Setup(x => x.CheckForSpoofingAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(SpoofingCheckResult.NotSuspicious()); - - // Default permission cache setup - _mockPermissionCache.Setup(x => x.GetOrSetPermissionsAsync( - It.IsAny(), - It.IsAny>>>(), - It.IsAny())) - .ReturnsAsync(new List { "basic:read" }); - - _handler = new LoginWithMagicLinkHandler( - _mockContext.Object, - _mockMagicLinkService.Object, - _mockUserRepository.Object, - _mockTokenService.Object, - _mockPermissionCache.Object, - _mockForceReauthService.Object, - _mockRevokedSessionService.Object, - _mockDeviceService.Object, - _mockAuditService.Object, - _mockMfaService.Object, - _mockEmailService.Object, - _mockEmailTemplateService.Object, - _mockConfiguration.Object, - _mockRiskScoringService.Object, - _mockLoginPatternService.Object, - _mockGeoIpService.Object, - _mockDeviceDetectionService.Object, - _mockLogger.Object); - } - - [Fact] - public async Task Handle_WithValidToken_ReturnsAuthResponse() - { - // Arrange - var user = TestDataFactory.CreateSystemUser(); - var magicLinkToken = TestDataFactory.CreateMagicLinkToken(user.Id); - magicLinkToken.User = user; - var device = TestDataFactory.CreateDevice(user.Id); - var command = new LoginWithMagicLinkCommand("valid-token"); - - _mockMagicLinkService.Setup(x => x.ValidateTokenAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(magicLinkToken); - _mockUserRepository.Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - _mockDeviceService.Setup(x => x.FindTrustedDeviceAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(device); - - // Act - var result = await _handler.Handle(command, CancellationToken.None); - - // Assert - result.AccessToken.Should().NotBeNullOrEmpty(); - result.RefreshToken.Should().NotBeNullOrEmpty(); - result.User.Should().NotBeNull(); - result.User!.Email.Should().Be(user.Email); - _mockMagicLinkService.Verify(x => x.MarkAsUsedAsync(magicLinkToken, It.IsAny()), Times.Once); - } - - [Fact] - public async Task Handle_WithInvalidToken_ThrowsMagicLinkTokenInvalidException() - { - // Arrange - var command = new LoginWithMagicLinkCommand("invalid-token"); - - _mockMagicLinkService.Setup(x => x.ValidateTokenAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync((MagicLinkToken?)null); - - // Act & Assert - await FluentActions.Invoking(() => _handler.Handle(command, CancellationToken.None).AsTask()) - .Should().ThrowAsync(); - - _mockAuditService.Verify(x => x.LogWithContextAsync( - AuditActions.MagicLinkLoginFailed, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Handle_WithInactiveUser_ThrowsMagicLinkTokenInvalidException() - { - // Arrange - var user = TestDataFactory.CreateSystemUser(); - user.Deactivate(); - var magicLinkToken = TestDataFactory.CreateMagicLinkToken(user.Id); - magicLinkToken.User = user; - var command = new LoginWithMagicLinkCommand("valid-token"); - - _mockMagicLinkService.Setup(x => x.ValidateTokenAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(magicLinkToken); - _mockUserRepository.Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - - // Act & Assert - await FluentActions.Invoking(() => _handler.Handle(command, CancellationToken.None).AsTask()) - .Should().ThrowAsync(); - } - - [Fact] - public async Task Handle_WithLockedUser_ThrowsMagicLinkTokenInvalidException() - { - // Arrange - var user = TestDataFactory.CreateSystemUser(); - user.Lock(DateTime.UtcNow.AddHours(1)); // Lock for 1 hour - var magicLinkToken = TestDataFactory.CreateMagicLinkToken(user.Id); - magicLinkToken.User = user; - var command = new LoginWithMagicLinkCommand("valid-token"); - - _mockMagicLinkService.Setup(x => x.ValidateTokenAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(magicLinkToken); - _mockUserRepository.Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - - // Act & Assert - await FluentActions.Invoking(() => _handler.Handle(command, CancellationToken.None).AsTask()) - .Should().ThrowAsync(); - } - - [Fact] - public async Task Handle_WithMfaEnabled_ReturnsMfaRequired() - { - // Arrange - var user = TestDataFactory.CreateSystemUser(); - user.EnableMfa(); - var magicLinkToken = TestDataFactory.CreateMagicLinkToken(user.Id); - magicLinkToken.User = user; - var command = new LoginWithMagicLinkCommand("valid-token"); - - _mockMagicLinkService.Setup(x => x.ValidateTokenAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(magicLinkToken); - _mockUserRepository.Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - _mockMfaService.Setup(x => x.GenerateMfaToken(It.IsAny(), It.IsAny())) - .Returns("mfa-token"); - - // Act - var result = await _handler.Handle(command, CancellationToken.None); - - // Assert - result.MfaRequired.Should().BeTrue(); - result.MfaToken.Should().Be("mfa-token"); - result.AccessToken.Should().BeNull(); - } - - [Fact] - public async Task Handle_WithSystemPermissionsAndNoMfa_ReturnsMfaSetupRequired() - { - // Arrange - var user = TestDataFactory.CreateSystemUser(); - var magicLinkToken = TestDataFactory.CreateMagicLinkToken(user.Id); - magicLinkToken.User = user; - var command = new LoginWithMagicLinkCommand("valid-token"); - - _mockMagicLinkService.Setup(x => x.ValidateTokenAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(magicLinkToken); - _mockUserRepository.Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - _mockPermissionCache.Setup(x => x.GetOrSetPermissionsAsync( - It.IsAny(), - It.IsAny>>>(), - It.IsAny())) - .ReturnsAsync(new List { "system:users:read" }); - _mockMfaService.Setup(x => x.GenerateMfaToken(It.IsAny(), It.IsAny())) - .Returns("setup-token"); - - // Act - var result = await _handler.Handle(command, CancellationToken.None); - - // Assert - result.MfaSetupRequired.Should().BeTrue(); - result.SetupToken.Should().Be("setup-token"); - result.AccessToken.Should().BeNull(); - } - - [Fact] - public async Task Handle_WithNewDevice_ReturnsDeviceApprovalRequired() - { - // Arrange - var user = TestDataFactory.CreateSystemUser(); - var magicLinkToken = TestDataFactory.CreateMagicLinkToken(user.Id); - magicLinkToken.User = user; - var pendingDevice = TestDataFactory.CreateDevice(user.Id); - var command = new LoginWithMagicLinkCommand("valid-token"); - - _mockMagicLinkService.Setup(x => x.ValidateTokenAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(magicLinkToken); - _mockUserRepository.Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - _mockDeviceService.Setup(x => x.FindTrustedDeviceAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync((Device?)null); // No trusted device found - _mockDeviceService.Setup(x => x.CreatePendingDeviceAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(new PendingDeviceResult(pendingDevice, "approval-token", "ABCD-1234")); - - // Act - var result = await _handler.Handle(command, CancellationToken.None); - - // Assert - result.DeviceApprovalRequired.Should().BeTrue(); - result.ApprovalToken.Should().Be("approval-token"); - result.AccessToken.Should().BeNull(); - } - - [Fact] - public async Task Handle_WithSuspiciousTrustedDevice_ReturnsDeviceApprovalRequired() - { - // Arrange - var user = TestDataFactory.CreateSystemUser(); - var magicLinkToken = TestDataFactory.CreateMagicLinkToken(user.Id); - magicLinkToken.User = user; - var device = TestDataFactory.CreateDevice(user.Id); - var pendingDevice = TestDataFactory.CreateDevice(user.Id); - var command = new LoginWithMagicLinkCommand("valid-token"); - - _mockMagicLinkService.Setup(x => x.ValidateTokenAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(magicLinkToken); - _mockUserRepository.Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - _mockDeviceService.Setup(x => x.FindTrustedDeviceAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(device); - _mockRiskScoringService.Setup(x => x.CheckForSpoofingAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(SpoofingCheckResult.Suspicious(85, new[] { "Device spoofing detected" })); - _mockDeviceService.Setup(x => x.CreatePendingDeviceAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(new PendingDeviceResult(pendingDevice, "approval-token", "ABCD-1234")); - - // Act - var result = await _handler.Handle(command, CancellationToken.None); - - // Assert - result.DeviceApprovalRequired.Should().BeTrue(); - } - - [Fact] - public async Task Handle_MarksTokenAsUsed() - { - // Arrange - var user = TestDataFactory.CreateSystemUser(); - var magicLinkToken = TestDataFactory.CreateMagicLinkToken(user.Id); - magicLinkToken.User = user; - var device = TestDataFactory.CreateDevice(user.Id); - var command = new LoginWithMagicLinkCommand("valid-token"); - - _mockMagicLinkService.Setup(x => x.ValidateTokenAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(magicLinkToken); - _mockUserRepository.Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - _mockDeviceService.Setup(x => x.FindTrustedDeviceAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(device); - - // Act - await _handler.Handle(command, CancellationToken.None); - - // Assert - _mockMagicLinkService.Verify(x => x.MarkAsUsedAsync(magicLinkToken, It.IsAny()), Times.Once); - } - - [Fact] - public async Task Handle_RecordsLoginPattern() - { - // Arrange - var user = TestDataFactory.CreateSystemUser(); - var magicLinkToken = TestDataFactory.CreateMagicLinkToken(user.Id); - magicLinkToken.User = user; - var device = TestDataFactory.CreateDevice(user.Id); - var command = new LoginWithMagicLinkCommand("valid-token", IpAddress: "192.168.1.1"); - - _mockMagicLinkService.Setup(x => x.ValidateTokenAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(magicLinkToken); - _mockUserRepository.Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - _mockDeviceService.Setup(x => x.FindTrustedDeviceAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(device); - - // Act - await _handler.Handle(command, CancellationToken.None); - - // Assert - _mockLoginPatternService.Verify(x => x.RecordLoginAsync( - user.Id, - It.IsAny(), - It.IsAny(), - "192.168.1.1", - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Handle_LogsSuccessfulLogin() - { - // Arrange - var user = TestDataFactory.CreateSystemUser(); - var magicLinkToken = TestDataFactory.CreateMagicLinkToken(user.Id); - magicLinkToken.User = user; - var device = TestDataFactory.CreateDevice(user.Id); - var command = new LoginWithMagicLinkCommand("valid-token"); - - _mockMagicLinkService.Setup(x => x.ValidateTokenAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(magicLinkToken); - _mockUserRepository.Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - _mockDeviceService.Setup(x => x.FindTrustedDeviceAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(device); - - // Act - await _handler.Handle(command, CancellationToken.None); - - // Assert - _mockAuditService.Verify(x => x.LogWithContextAsync( - AuditActions.MagicLinkLogin, - user.Id, - It.IsAny(), - "SystemUser", - user.Id, - It.IsAny(), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Handle_UpdatesUserLastLogin() - { - // Arrange - var user = TestDataFactory.CreateSystemUser(); - var magicLinkToken = TestDataFactory.CreateMagicLinkToken(user.Id); - magicLinkToken.User = user; - var device = TestDataFactory.CreateDevice(user.Id); - var command = new LoginWithMagicLinkCommand("valid-token"); - - _mockMagicLinkService.Setup(x => x.ValidateTokenAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(magicLinkToken); - _mockUserRepository.Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - _mockDeviceService.Setup(x => x.FindTrustedDeviceAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(device); - - // Act - await _handler.Handle(command, CancellationToken.None); - - // Assert - _mockUserRepository.Verify(x => x.UpdateAsync( - It.Is(u => u.LastLoginAt != null), - It.IsAny()), Times.Once); - } -} diff --git a/backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/MagicLinkTokenTests.cs b/backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/MagicLinkTokenTests.cs deleted file mode 100644 index c770689..0000000 --- a/backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/MagicLinkTokenTests.cs +++ /dev/null @@ -1,189 +0,0 @@ -using ExoAuth.Domain.Entities; -using FluentAssertions; - -namespace ExoAuth.UnitTests.Features.Auth.MagicLink; - -public sealed class MagicLinkTokenTests -{ - [Fact] - public void Create_WithValidParameters_CreatesToken() - { - // Arrange - var userId = Guid.NewGuid(); - var token = MagicLinkToken.GenerateToken(); - var expirationMinutes = 15; - - // Act - var magicLinkToken = MagicLinkToken.Create(userId, token, expirationMinutes); - - // Assert - magicLinkToken.UserId.Should().Be(userId); - magicLinkToken.TokenHash.Should().NotBeNullOrEmpty(); - magicLinkToken.IsUsed.Should().BeFalse(); - magicLinkToken.UsedAt.Should().BeNull(); - magicLinkToken.ExpiresAt.Should().BeCloseTo(DateTime.UtcNow.AddMinutes(expirationMinutes), TimeSpan.FromSeconds(5)); - } - - [Fact] - public void Create_HashesToken() - { - // Arrange - var userId = Guid.NewGuid(); - var token = "test-token-value"; - - // Act - var magicLinkToken = MagicLinkToken.Create(userId, token, 15); - - // Assert - magicLinkToken.TokenHash.Should().NotBe(token); - magicLinkToken.TokenHash.Should().NotBeNullOrEmpty(); - } - - [Fact] - public void GenerateToken_ReturnsUniqueTokens() - { - // Act - var token1 = MagicLinkToken.GenerateToken(); - var token2 = MagicLinkToken.GenerateToken(); - - // Assert - token1.Should().NotBe(token2); - token1.Should().HaveLength(43); // Base64 of 32 bytes without padding - } - - [Fact] - public void GenerateToken_ReturnsUrlSafeToken() - { - // Act - var token = MagicLinkToken.GenerateToken(); - - // Assert - token.Should().NotContain("+"); - token.Should().NotContain("/"); - token.Should().NotContain("="); - } - - [Fact] - public void IsExpired_WhenNotExpired_ReturnsFalse() - { - // Arrange - var userId = Guid.NewGuid(); - var token = MagicLinkToken.GenerateToken(); - var magicLinkToken = MagicLinkToken.Create(userId, token, 15); - - // Act & Assert - magicLinkToken.IsExpired.Should().BeFalse(); - } - - [Fact] - public void IsExpired_WhenExpired_ReturnsTrue() - { - // Arrange - var userId = Guid.NewGuid(); - var token = MagicLinkToken.GenerateToken(); - var magicLinkToken = MagicLinkToken.Create(userId, token, -1); // Already expired - - // Act & Assert - magicLinkToken.IsExpired.Should().BeTrue(); - } - - [Fact] - public void IsValid_WhenNotUsedAndNotExpired_ReturnsTrue() - { - // Arrange - var userId = Guid.NewGuid(); - var token = MagicLinkToken.GenerateToken(); - var magicLinkToken = MagicLinkToken.Create(userId, token, 15); - - // Act & Assert - magicLinkToken.IsValid.Should().BeTrue(); - } - - [Fact] - public void IsValid_WhenUsed_ReturnsFalse() - { - // Arrange - var userId = Guid.NewGuid(); - var token = MagicLinkToken.GenerateToken(); - var magicLinkToken = MagicLinkToken.Create(userId, token, 15); - magicLinkToken.MarkAsUsed(); - - // Act & Assert - magicLinkToken.IsValid.Should().BeFalse(); - } - - [Fact] - public void IsValid_WhenExpired_ReturnsFalse() - { - // Arrange - var userId = Guid.NewGuid(); - var token = MagicLinkToken.GenerateToken(); - var magicLinkToken = MagicLinkToken.Create(userId, token, -1); - - // Act & Assert - magicLinkToken.IsValid.Should().BeFalse(); - } - - [Fact] - public void MarkAsUsed_SetsIsUsedAndUsedAt() - { - // Arrange - var userId = Guid.NewGuid(); - var token = MagicLinkToken.GenerateToken(); - var magicLinkToken = MagicLinkToken.Create(userId, token, 15); - - // Act - magicLinkToken.MarkAsUsed(); - - // Assert - magicLinkToken.IsUsed.Should().BeTrue(); - magicLinkToken.UsedAt.Should().NotBeNull(); - magicLinkToken.UsedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5)); - } - - [Fact] - public void ValidateToken_WithCorrectToken_ReturnsTrue() - { - // Arrange - var userId = Guid.NewGuid(); - var token = "test-token-value"; - var magicLinkToken = MagicLinkToken.Create(userId, token, 15); - - // Act - var result = magicLinkToken.ValidateToken(token); - - // Assert - result.Should().BeTrue(); - } - - [Fact] - public void ValidateToken_WithIncorrectToken_ReturnsFalse() - { - // Arrange - var userId = Guid.NewGuid(); - var token = "test-token-value"; - var magicLinkToken = MagicLinkToken.Create(userId, token, 15); - - // Act - var result = magicLinkToken.ValidateToken("wrong-token"); - - // Assert - result.Should().BeFalse(); - } - - [Fact] - public void Create_WithDifferentExpirationMinutes_SetsCorrectExpiry() - { - // Arrange - var userId = Guid.NewGuid(); - var token = MagicLinkToken.GenerateToken(); - - // Act - var magicLinkToken5 = MagicLinkToken.Create(userId, token, 5); - var magicLinkToken30 = MagicLinkToken.Create(userId, token, 30); - - // Assert - magicLinkToken5.ExpiresAt.Should().BeCloseTo(DateTime.UtcNow.AddMinutes(5), TimeSpan.FromSeconds(5)); - magicLinkToken30.ExpiresAt.Should().BeCloseTo(DateTime.UtcNow.AddMinutes(30), TimeSpan.FromSeconds(5)); - } -} diff --git a/backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/MagicLinkValidatorTests.cs b/backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/MagicLinkValidatorTests.cs deleted file mode 100644 index 4c78fe4..0000000 --- a/backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/MagicLinkValidatorTests.cs +++ /dev/null @@ -1,216 +0,0 @@ -using ExoAuth.Application.Features.Auth.Commands.LoginWithMagicLink; -using ExoAuth.Application.Features.Auth.Commands.RequestMagicLink; -using FluentAssertions; -using FluentValidation.TestHelper; - -namespace ExoAuth.UnitTests.Features.Auth.MagicLink; - -public sealed class MagicLinkValidatorTests -{ - private readonly RequestMagicLinkValidator _requestValidator; - private readonly LoginWithMagicLinkValidator _loginValidator; - - public MagicLinkValidatorTests() - { - _requestValidator = new RequestMagicLinkValidator(); - _loginValidator = new LoginWithMagicLinkValidator(); - } - - #region RequestMagicLinkValidator Tests - - [Fact] - public void RequestMagicLinkValidator_WithValidEmail_PassesValidation() - { - // Arrange - var command = new RequestMagicLinkCommand("test@example.com"); - - // Act - var result = _requestValidator.TestValidate(command); - - // Assert - result.ShouldNotHaveAnyValidationErrors(); - } - - [Fact] - public void RequestMagicLinkValidator_WithEmptyEmail_FailsValidation() - { - // Arrange - var command = new RequestMagicLinkCommand(""); - - // Act - var result = _requestValidator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.Email) - .WithErrorMessage("Email is required"); - } - - [Fact] - public void RequestMagicLinkValidator_WithNullEmail_FailsValidation() - { - // Arrange - var command = new RequestMagicLinkCommand(null!); - - // Act - var result = _requestValidator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.Email); - } - - [Fact] - public void RequestMagicLinkValidator_WithInvalidEmailFormat_FailsValidation() - { - // Arrange - var command = new RequestMagicLinkCommand("not-an-email"); - - // Act - var result = _requestValidator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.Email) - .WithErrorMessage("Invalid email format"); - } - - [Theory] - [InlineData("test@example.com")] - [InlineData("user.name@domain.co.uk")] - [InlineData("user+tag@example.org")] - public void RequestMagicLinkValidator_WithVariousValidEmails_PassesValidation(string email) - { - // Arrange - var command = new RequestMagicLinkCommand(email); - - // Act - var result = _requestValidator.TestValidate(command); - - // Assert - result.ShouldNotHaveAnyValidationErrors(); - } - - [Theory] - [InlineData("plaintext")] - [InlineData("@nodomain")] - public void RequestMagicLinkValidator_WithVariousInvalidEmails_FailsValidation(string email) - { - // Arrange - var command = new RequestMagicLinkCommand(email); - - // Act - var result = _requestValidator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.Email); - } - - #endregion - - #region LoginWithMagicLinkValidator Tests - - [Fact] - public void LoginWithMagicLinkValidator_WithValidToken_PassesValidation() - { - // Arrange - var command = new LoginWithMagicLinkCommand("valid-token-value"); - - // Act - var result = _loginValidator.TestValidate(command); - - // Assert - result.ShouldNotHaveAnyValidationErrors(); - } - - [Fact] - public void LoginWithMagicLinkValidator_WithEmptyToken_FailsValidation() - { - // Arrange - var command = new LoginWithMagicLinkCommand(""); - - // Act - var result = _loginValidator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.Token) - .WithErrorMessage("Magic link token is required"); - } - - [Fact] - public void LoginWithMagicLinkValidator_WithNullToken_FailsValidation() - { - // Arrange - var command = new LoginWithMagicLinkCommand(null!); - - // Act - var result = _loginValidator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.Token); - } - - [Fact] - public void LoginWithMagicLinkValidator_WithWhitespaceToken_FailsValidation() - { - // Arrange - var command = new LoginWithMagicLinkCommand(" "); - - // Act - var result = _loginValidator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.Token); - } - - [Fact] - public void LoginWithMagicLinkValidator_AllowsOptionalDeviceId() - { - // Arrange - var command = new LoginWithMagicLinkCommand("valid-token", DeviceId: "device-123"); - - // Act - var result = _loginValidator.TestValidate(command); - - // Assert - result.ShouldNotHaveAnyValidationErrors(); - } - - [Fact] - public void LoginWithMagicLinkValidator_AllowsOptionalDeviceFingerprint() - { - // Arrange - var command = new LoginWithMagicLinkCommand("valid-token", DeviceFingerprint: "fingerprint-xyz"); - - // Act - var result = _loginValidator.TestValidate(command); - - // Assert - result.ShouldNotHaveAnyValidationErrors(); - } - - [Fact] - public void LoginWithMagicLinkValidator_AllowsOptionalUserAgent() - { - // Arrange - var command = new LoginWithMagicLinkCommand("valid-token", UserAgent: "Mozilla/5.0"); - - // Act - var result = _loginValidator.TestValidate(command); - - // Assert - result.ShouldNotHaveAnyValidationErrors(); - } - - [Fact] - public void LoginWithMagicLinkValidator_AllowsRememberMeFlag() - { - // Arrange - var command = new LoginWithMagicLinkCommand("valid-token", RememberMe: true); - - // Act - var result = _loginValidator.TestValidate(command); - - // Assert - result.ShouldNotHaveAnyValidationErrors(); - } - - #endregion -} diff --git a/backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/RequestMagicLinkHandlerTests.cs b/backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/RequestMagicLinkHandlerTests.cs deleted file mode 100644 index ce7430e..0000000 --- a/backend/tests/ExoAuth.UnitTests/Features/Auth/MagicLink/RequestMagicLinkHandlerTests.cs +++ /dev/null @@ -1,239 +0,0 @@ -using ExoAuth.Application.Common.Interfaces; -using ExoAuth.Application.Features.Auth.Commands.RequestMagicLink; -using ExoAuth.Domain.Entities; -using ExoAuth.UnitTests.Helpers; -using FluentAssertions; -using Microsoft.Extensions.Logging; -using Moq; - -namespace ExoAuth.UnitTests.Features.Auth.MagicLink; - -public sealed class RequestMagicLinkHandlerTests -{ - private readonly Mock _mockUserRepository; - private readonly Mock _mockMagicLinkService; - private readonly Mock _mockEmailService; - private readonly Mock _mockAuditService; - private readonly Mock _mockCaptchaService; - private readonly Mock> _mockLogger; - private readonly RequestMagicLinkHandler _handler; - - public RequestMagicLinkHandlerTests() - { - _mockUserRepository = new Mock(); - _mockMagicLinkService = new Mock(); - _mockEmailService = new Mock(); - _mockAuditService = new Mock(); - _mockCaptchaService = new Mock(); - _mockLogger = new Mock>(); - - // Default CAPTCHA service setup - always valid in tests - _mockCaptchaService.Setup(x => x.ValidateRequiredAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(Task.CompletedTask); - - _handler = new RequestMagicLinkHandler( - _mockUserRepository.Object, - _mockMagicLinkService.Object, - _mockEmailService.Object, - _mockAuditService.Object, - _mockCaptchaService.Object, - _mockLogger.Object); - } - - [Fact] - public async Task Handle_WithValidEmail_CreatesMagicLinkAndSendsEmail() - { - // Arrange - var command = new RequestMagicLinkCommand("test@example.com", "captcha-token"); - var user = TestDataFactory.CreateSystemUser(email: "test@example.com"); - var magicLinkToken = TestDataFactory.CreateMagicLinkToken(user.Id); - var magicLinkResult = new MagicLinkResult(magicLinkToken, "test-token-value"); - - _mockUserRepository.Setup(x => x.GetByEmailAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - _mockMagicLinkService.Setup(x => x.CreateMagicLinkAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(magicLinkResult); - - // Act - var result = await _handler.Handle(command, CancellationToken.None); - - // Assert - result.Success.Should().BeTrue(); - _mockMagicLinkService.Verify(x => x.CreateMagicLinkAsync(user.Id, It.IsAny()), Times.Once); - _mockEmailService.Verify(x => x.SendMagicLinkAsync( - user.Email, - user.FirstName, - "test-token-value", - user.Id, - It.IsAny(), - It.IsAny()), Times.Once); - _mockAuditService.Verify(x => x.LogWithContextAsync( - AuditActions.MagicLinkRequested, - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Handle_WithNonExistentEmail_ReturnsSuccessWithoutSendingEmail() - { - // Arrange - var command = new RequestMagicLinkCommand("notfound@example.com", "captcha-token"); - - _mockUserRepository.Setup(x => x.GetByEmailAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync((SystemUser?)null); - - // Act - var result = await _handler.Handle(command, CancellationToken.None); - - // Assert - Returns success to prevent email enumeration - result.Success.Should().BeTrue(); - _mockMagicLinkService.Verify(x => x.CreateMagicLinkAsync(It.IsAny(), It.IsAny()), Times.Never); - _mockEmailService.Verify(x => x.SendMagicLinkAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()), Times.Never); - } - - [Fact] - public async Task Handle_WithInactiveUser_ReturnsSuccessWithoutSendingEmail() - { - // Arrange - var command = new RequestMagicLinkCommand("inactive@example.com", "captcha-token"); - var user = TestDataFactory.CreateSystemUser(email: "inactive@example.com"); - user.Deactivate(); - - _mockUserRepository.Setup(x => x.GetByEmailAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - - // Act - var result = await _handler.Handle(command, CancellationToken.None); - - // Assert - Returns success to prevent user enumeration - result.Success.Should().BeTrue(); - _mockMagicLinkService.Verify(x => x.CreateMagicLinkAsync(It.IsAny(), It.IsAny()), Times.Never); - _mockEmailService.Verify(x => x.SendMagicLinkAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()), Times.Never); - } - - [Fact] - public async Task Handle_WithAnonymizedUser_ReturnsSuccessWithoutSendingEmail() - { - // Arrange - var command = new RequestMagicLinkCommand("anonymized@example.com", "captcha-token"); - var user = TestDataFactory.CreateSystemUser(email: "anonymized@example.com"); - user.Anonymize(); - - _mockUserRepository.Setup(x => x.GetByEmailAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - - // Act - var result = await _handler.Handle(command, CancellationToken.None); - - // Assert - Returns success to prevent user enumeration - result.Success.Should().BeTrue(); - _mockMagicLinkService.Verify(x => x.CreateMagicLinkAsync(It.IsAny(), It.IsAny()), Times.Never); - _mockEmailService.Verify(x => x.SendMagicLinkAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()), Times.Never); - } - - [Fact] - public async Task Handle_EmailIsNormalizedToLowercase() - { - // Arrange - var command = new RequestMagicLinkCommand("TEST@EXAMPLE.COM", "captcha-token"); - - _mockUserRepository.Setup(x => x.GetByEmailAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync((SystemUser?)null); - - // Act - await _handler.Handle(command, CancellationToken.None); - - // Assert - _mockUserRepository.Verify(x => x.GetByEmailAsync("test@example.com", It.IsAny()), Times.Once); - } - - [Fact] - public async Task Handle_ReturnsCorrectMessage() - { - // Arrange - var command = new RequestMagicLinkCommand("test@example.com", "captcha-token"); - - _mockUserRepository.Setup(x => x.GetByEmailAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync((SystemUser?)null); - - // Act - var result = await _handler.Handle(command, CancellationToken.None); - - // Assert - result.Message.Should().Contain("If an account exists with this email"); - } - - [Fact] - public async Task Handle_ValidatesCaptcha() - { - // Arrange - var command = new RequestMagicLinkCommand("test@example.com", "captcha-token", "192.168.1.1"); - - _mockUserRepository.Setup(x => x.GetByEmailAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync((SystemUser?)null); - - // Act - await _handler.Handle(command, CancellationToken.None); - - // Assert - _mockCaptchaService.Verify(x => x.ValidateRequiredAsync( - "captcha-token", - "magic_link", - "192.168.1.1", - It.IsAny()), Times.Once); - } - - [Fact] - public async Task Handle_UsesUserPreferredLanguageForEmail() - { - // Arrange - var command = new RequestMagicLinkCommand("test@example.com", "captcha-token"); - var user = TestDataFactory.CreateSystemUser(email: "test@example.com"); - user.SetPreferredLanguage("de-DE"); - var magicLinkToken = TestDataFactory.CreateMagicLinkToken(user.Id); - var magicLinkResult = new MagicLinkResult(magicLinkToken, "test-token-value"); - - _mockUserRepository.Setup(x => x.GetByEmailAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(user); - _mockMagicLinkService.Setup(x => x.CreateMagicLinkAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(magicLinkResult); - - // Act - await _handler.Handle(command, CancellationToken.None); - - // Assert - _mockEmailService.Verify(x => x.SendMagicLinkAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - "de-DE", - It.IsAny()), Times.Once); - } -} diff --git a/backend/tests/ExoAuth.UnitTests/Helpers/MockDbContext.cs b/backend/tests/ExoAuth.UnitTests/Helpers/MockDbContext.cs index 3155c9a..6db0632 100644 --- a/backend/tests/ExoAuth.UnitTests/Helpers/MockDbContext.cs +++ b/backend/tests/ExoAuth.UnitTests/Helpers/MockDbContext.cs @@ -27,7 +27,6 @@ public static Mock Create() mockContext.Setup(x => x.LoginPatterns).Returns(CreateAsyncMockDbSet(new List()).Object); mockContext.Setup(x => x.Passkeys).Returns(CreateAsyncMockDbSet(new List()).Object); mockContext.Setup(x => x.IpRestrictions).Returns(CreateAsyncMockDbSet(new List()).Object); - mockContext.Setup(x => x.MagicLinkTokens).Returns(CreateAsyncMockDbSet(new List()).Object); // Email DbSets mockContext.Setup(x => x.EmailProviders).Returns(CreateAsyncMockDbSet(new List()).Object); diff --git a/backend/tests/ExoAuth.UnitTests/Helpers/TestDataFactory.cs b/backend/tests/ExoAuth.UnitTests/Helpers/TestDataFactory.cs index 56bd0a5..6fa9004 100644 --- a/backend/tests/ExoAuth.UnitTests/Helpers/TestDataFactory.cs +++ b/backend/tests/ExoAuth.UnitTests/Helpers/TestDataFactory.cs @@ -258,40 +258,4 @@ public static PasswordResetToken CreatePasswordResetToken( { return PasswordResetToken.Create(userId, token, code, expirationMinutes); } - - // Magic Link Token - public static MagicLinkToken CreateMagicLinkToken( - Guid userId, - string? token = null, - int expirationMinutes = 15) - { - var tokenValue = token ?? MagicLinkToken.GenerateToken(); - return MagicLinkToken.Create(userId, tokenValue, expirationMinutes); - } - - public static MagicLinkToken CreateMagicLinkTokenWithId( - Guid id, - Guid userId, - string? token = null, - int expirationMinutes = 15) - { - var magicLinkToken = CreateMagicLinkToken(userId, token, expirationMinutes); - SetEntityId(magicLinkToken, id); - return magicLinkToken; - } - - public static MagicLinkToken CreateExpiredMagicLinkToken(Guid userId) - { - var token = MagicLinkToken.GenerateToken(); - var magicLinkToken = MagicLinkToken.Create(userId, token, -1); // Already expired - return magicLinkToken; - } - - public static MagicLinkToken CreateUsedMagicLinkToken(Guid userId) - { - var token = MagicLinkToken.GenerateToken(); - var magicLinkToken = MagicLinkToken.Create(userId, token, 15); - magicLinkToken.MarkAsUsed(); - return magicLinkToken; - } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ba65757..75deae2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,12 +8,7 @@ "name": "frontend", "version": "0.0.0", "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@hcaptcha/react-hcaptcha": "^1.17.4", "@hookform/resolvers": "^5.2.2", - "@marsidev/react-turnstile": "^1.4.1", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", @@ -29,14 +24,9 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "@simplewebauthn/browser": "^13.2.2", "@tanstack/react-query": "^5.90.12", "@tanstack/react-router": "^1.143.4", "@tanstack/react-table": "^8.21.3", - "@tiptap/extension-link": "^3.15.3", - "@tiptap/extension-placeholder": "^3.15.3", - "@tiptap/react": "^3.15.3", - "@tiptap/starter-kit": "^3.15.3", "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -50,7 +40,6 @@ "react": "^19.2.0", "react-day-picker": "^9.13.0", "react-dom": "^19.2.0", - "react-google-recaptcha-v3": "^1.11.0", "react-hook-form": "^7.69.0", "react-i18next": "^16.5.0", "react-intersection-observer": "^10.0.0", @@ -111,16 +100,6 @@ "lru-cache": "^11.2.4" } }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@asamuzakjp/dom-selector": { "version": "6.7.6", "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz", @@ -135,16 +114,6 @@ "lru-cache": "^11.2.4" } }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@asamuzakjp/nwsapi": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", @@ -242,6 +211,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -584,648 +563,170 @@ "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", "license": "MIT" }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", - "license": "MIT", - "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/sortable": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", - "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", - "license": "MIT", - "dependencies": { - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@dnd-kit/core": "^6.3.0", - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { + "node_modules/@esbuild/win32-x64": { "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ - "ppc64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "aix" + "win32" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1274,26 +775,6 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, - "node_modules/@hcaptcha/loader": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@hcaptcha/loader/-/loader-2.3.0.tgz", - "integrity": "sha512-i4lnNxKBe+COf3R1nFZEWaZoHIoJjvDgWqvcNrdZq8ehoSNMN6KVZ56dcQ02qKie2h3+BkbkwlJA9DOIuLlK/g==", - "license": "MIT" - }, - "node_modules/@hcaptcha/react-hcaptcha": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.17.4.tgz", - "integrity": "sha512-rIvgesG1N7SS9sAYYHFoWm+nXqRrxq7RcA9z2pKkDWV+S1GdfmrTNYA1aPyVWVe3eowphTCwyDJvl97Swwy0mw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.17.9", - "@hcaptcha/loader": "^2.3.0" - }, - "peerDependencies": { - "react": ">= 16.3.0", - "react-dom": ">= 16.3.0" - } - }, "node_modules/@hookform/resolvers": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", @@ -1408,16 +889,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@marsidev/react-turnstile": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@marsidev/react-turnstile/-/react-turnstile-1.4.1.tgz", - "integrity": "sha512-1jE0IjvB8z+q1NFRs3149gXzXwIzXQWqQjn9fmAr13BiE3RYLWck5Me6flHYE90shW5L12Jkm6R1peS1OnA9oQ==", - "license": "MIT", - "peerDependencies": { - "react": "^17.0.2 || ^18.0.0 || ^19.0", - "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" - } - }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -2603,369 +2074,83 @@ "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@remirror/core-constants": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", - "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", - "license": "MIT" - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", - "cpu": [ - "ia32" - ], + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.54.0", @@ -2995,12 +2180,6 @@ "win32" ] }, - "node_modules/@simplewebauthn/browser": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.2.2.tgz", - "integrity": "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==", - "license": "MIT" - }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -3034,224 +2213,24 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dev": true, + "license": "MIT", "engines": { "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { @@ -3473,6 +2452,16 @@ "yarn": ">=1" } }, + "node_modules/@testing-library/jest-dom/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", @@ -3485,488 +2474,41 @@ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.1.tgz", "integrity": "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@tiptap/core": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.15.3.tgz", - "integrity": "sha512-bmXydIHfm2rEtGju39FiQNfzkFx9CDvJe+xem1dgEZ2P6Dj7nQX9LnA1ZscW7TuzbBRkL5p3dwuBIi3f62A66A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/pm": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-blockquote": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.15.3.tgz", - "integrity": "sha512-13x5UsQXtttFpoS/n1q173OeurNxppsdWgP3JfsshzyxIghhC141uL3H6SGYQLPU31AizgDs2OEzt6cSUevaZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-bold": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.15.3.tgz", - "integrity": "sha512-I8JYbkkUTNUXbHd/wCse2bR0QhQtJD7+0/lgrKOmGfv5ioLxcki079Nzuqqay3PjgYoJLIJQvm3RAGxT+4X91w==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-bubble-menu": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.15.3.tgz", - "integrity": "sha512-e88DG1bTy6hKxrt7iPVQhJnH5/EOrnKpIyp09dfRDgWrrW88fE0Qjys7a/eT8W+sXyXM3z10Ye7zpERWsrLZDg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3", - "@tiptap/pm": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-bullet-list": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.15.3.tgz", - "integrity": "sha512-MGwEkNT7ltst6XaWf0ObNgpKQ4PvuuV3igkBrdYnQS+qaAx9IF4isygVPqUc9DvjYC306jpyKsNqNrENIXcosA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/extension-list": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-code": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.15.3.tgz", - "integrity": "sha512-x6LFt3Og6MFINYpsMzrJnz7vaT9Yk1t4oXkbJsJRSavdIWBEBcoRudKZ4sSe/AnsYlRJs8FY2uR76mt9e+7xAQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-code-block": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.15.3.tgz", - "integrity": "sha512-q1UB9icNfdJppTqMIUWfoRKkx5SSdWIpwZoL2NeOI5Ah3E20/dQKVttIgLhsE521chyvxCYCRaHD5tMNGKfhyw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3", - "@tiptap/pm": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-document": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.15.3.tgz", - "integrity": "sha512-AC72nI2gnogBuETCKbZekn+h6t5FGGcZG2abPGKbz/x9rwpb6qV2hcbAQ30t6M7H6cTOh2/Ut8bEV2MtMB15sw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-dropcursor": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.15.3.tgz", - "integrity": "sha512-jGI5XZpdo8GSYQFj7HY15/oEwC2m2TqZz0/Fln5qIhY32XlZhWrsMuMI6WbUJrTH16es7xO6jmRlDsc6g+vJWg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/extensions": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-floating-menu": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.15.3.tgz", - "integrity": "sha512-+3DVBleKKffadEJEdLYxmYAJOjHjLSqtiSFUE3RABT4V2ka1ODy2NIpyKX0o1SvQ5N1jViYT9Q+yUbNa6zCcDw==", - "license": "MIT", - "optional": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@floating-ui/dom": "^1.0.0", - "@tiptap/core": "^3.15.3", - "@tiptap/pm": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-gapcursor": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.15.3.tgz", - "integrity": "sha512-Kaw0sNzP0bQI/xEAMSfIpja6xhsu9WqqAK/puzOIS1RKWO47Wps/tzqdSJ9gfslPIb5uY5mKCfy8UR8Xgiia8w==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/extensions": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-hard-break": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.15.3.tgz", - "integrity": "sha512-8HjxmeRbBiXW+7JKemAJtZtHlmXQ9iji398CPQ0yYde68WbIvUhHXjmbJE5pxFvvQTJ/zJv1aISeEOZN2bKBaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-heading": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.15.3.tgz", - "integrity": "sha512-G1GG6iN1YXPS+75arDpo+bYRzhr3dNDw99c7D7na3aDawa9Qp7sZ/bVrzFUUcVEce0cD6h83yY7AooBxEc67hA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-horizontal-rule": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.15.3.tgz", - "integrity": "sha512-FYkN7L6JsfwwNEntmLklCVKvgL0B0N47OXMacRk6kYKQmVQ4Nvc7q/VJLpD9sk4wh4KT1aiCBfhKEBTu5pv1fg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3", - "@tiptap/pm": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-italic": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.15.3.tgz", - "integrity": "sha512-6XeuPjcWy7OBxpkgOV7bD6PATO5jhIxc8SEK4m8xn8nelGTBIbHGqK37evRv+QkC7E0MUryLtzwnmmiaxcKL0Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-link": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.15.3.tgz", - "integrity": "sha512-PdDXyBF9Wco9U1x6e+b7tKBWG+kqBDXDmaYXHkFm/gYuQCQafVJ5mdrDdKgkHDWVnJzMWZXBcZjT9r57qtlLWg==", - "license": "MIT", - "dependencies": { - "linkifyjs": "^4.3.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3", - "@tiptap/pm": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-list": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.15.3.tgz", - "integrity": "sha512-n7y/MF9lAM5qlpuH5IR4/uq+kJPEJpe9NrEiH+NmkO/5KJ6cXzpJ6F4U17sMLf2SNCq+TWN9QK8QzoKxIn50VQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3", - "@tiptap/pm": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-list-item": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.15.3.tgz", - "integrity": "sha512-CCxL5ek1p0lO5e8aqhnPzIySldXRSigBFk2fP9OLgdl5qKFLs2MGc19jFlx5+/kjXnEsdQTFbGY1Sizzt0TVDw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/extension-list": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-list-keymap": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.15.3.tgz", - "integrity": "sha512-UxqnTEEAKrL+wFQeSyC9z0mgyUUVRS2WTcVFoLZCE6/Xus9F53S4bl7VKFadjmqI4GpDk5Oe2IOUc72o129jWg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/extension-list": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-ordered-list": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.15.3.tgz", - "integrity": "sha512-/8uhw528Iy0c9wF6tHCiIn0ToM0Ml6Ll2c/3iPRnKr4IjXwx2Lr994stUFihb+oqGZwV1J8CPcZJ4Ufpdqi4Dw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/extension-list": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-paragraph": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.15.3.tgz", - "integrity": "sha512-lc0Qu/1AgzcEfS67NJMj5tSHHhH6NtA6uUpvppEKGsvJwgE2wKG1onE4isrVXmcGRdxSMiCtyTDemPNMu6/ozQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-placeholder": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.15.3.tgz", - "integrity": "sha512-XcHHnojT186hKIoOgcPBesXk89+caNGVUdMtc171Vcr/5s0dpnr4q5LfE+YRC+S85CpCxCRRnh84Ou+XRtOqrw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/extensions": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-strike": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.15.3.tgz", - "integrity": "sha512-Y1P3eGNY7RxQs2BcR6NfLo9VfEOplXXHAqkOM88oowWWOE7dMNeFFZM9H8HNxoQgXJ7H0aWW9B7ZTWM9hWli2Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-text": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.15.3.tgz", - "integrity": "sha512-MhkBz8ZvrqOKtKNp+ZWISKkLUlTrDR7tbKZc2OnNcUTttL9dz0HwT+cg91GGz19fuo7ttDcfsPV6eVmflvGToA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3" - } - }, - "node_modules/@tiptap/extension-underline": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.15.3.tgz", - "integrity": "sha512-r/IwcNN0W366jGu4Y0n2MiFq9jGa4aopOwtfWO4d+J0DyeS2m7Go3+KwoUqi0wQTiVU74yfi4DF6eRsMQ9/iHQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3" - } - }, - "node_modules/@tiptap/extensions": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.15.3.tgz", - "integrity": "sha512-ycx/BgxR4rc9tf3ZyTdI98Z19yKLFfqM3UN+v42ChuIwkzyr9zyp7kG8dB9xN2lNqrD+5y/HyJobz/VJ7T90gA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^3.15.3", - "@tiptap/pm": "^3.15.3" - } - }, - "node_modules/@tiptap/pm": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.15.3.tgz", - "integrity": "sha512-Zm1BaU1TwFi3CQiisxjgnzzIus+q40bBKWLqXf6WEaus8Z6+vo1MT2pU52dBCMIRaW9XNDq3E5cmGtMc1AlveA==", - "license": "MIT", - "dependencies": { - "prosemirror-changeset": "^2.3.0", - "prosemirror-collab": "^1.3.1", - "prosemirror-commands": "^1.6.2", - "prosemirror-dropcursor": "^1.8.1", - "prosemirror-gapcursor": "^1.3.2", - "prosemirror-history": "^1.4.1", - "prosemirror-inputrules": "^1.4.0", - "prosemirror-keymap": "^1.2.2", - "prosemirror-markdown": "^1.13.1", - "prosemirror-menu": "^1.2.4", - "prosemirror-model": "^1.24.1", - "prosemirror-schema-basic": "^1.2.3", - "prosemirror-schema-list": "^1.5.0", - "prosemirror-state": "^1.4.3", - "prosemirror-tables": "^1.6.4", - "prosemirror-trailing-node": "^3.0.0", - "prosemirror-transform": "^1.10.2", - "prosemirror-view": "^1.38.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - } - }, - "node_modules/@tiptap/react": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.15.3.tgz", - "integrity": "sha512-XvouB+Hrqw8yFmZLPEh+HWlMeRSjZfHSfWfWuw5d8LSwnxnPeu3Bg/rjHrRrdwb+7FumtzOnNWMorpb/PSOttQ==", - "license": "MIT", - "dependencies": { - "@types/use-sync-external-store": "^0.0.6", - "fast-equals": "^5.3.3", - "use-sync-external-store": "^1.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" }, - "optionalDependencies": { - "@tiptap/extension-bubble-menu": "^3.15.3", - "@tiptap/extension-floating-menu": "^3.15.3" + "engines": { + "node": ">=18" }, "peerDependencies": { - "@tiptap/core": "^3.15.3", - "@tiptap/pm": "^3.15.3", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@tiptap/starter-kit": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.15.3.tgz", - "integrity": "sha512-ia+eQr9Mt1ln2UO+kK4kFTJOrZK4GhvZXFjpCCYuHtco3rhr2fZAIxEEY4cl/vo5VO5WWyPqxhkFeLcoWmNjSw==", - "license": "MIT", - "dependencies": { - "@tiptap/core": "^3.15.3", - "@tiptap/extension-blockquote": "^3.15.3", - "@tiptap/extension-bold": "^3.15.3", - "@tiptap/extension-bullet-list": "^3.15.3", - "@tiptap/extension-code": "^3.15.3", - "@tiptap/extension-code-block": "^3.15.3", - "@tiptap/extension-document": "^3.15.3", - "@tiptap/extension-dropcursor": "^3.15.3", - "@tiptap/extension-gapcursor": "^3.15.3", - "@tiptap/extension-hard-break": "^3.15.3", - "@tiptap/extension-heading": "^3.15.3", - "@tiptap/extension-horizontal-rule": "^3.15.3", - "@tiptap/extension-italic": "^3.15.3", - "@tiptap/extension-link": "^3.15.3", - "@tiptap/extension-list": "^3.15.3", - "@tiptap/extension-list-item": "^3.15.3", - "@tiptap/extension-list-keymap": "^3.15.3", - "@tiptap/extension-ordered-list": "^3.15.3", - "@tiptap/extension-paragraph": "^3.15.3", - "@tiptap/extension-strike": "^3.15.3", - "@tiptap/extension-text": "^3.15.3", - "@tiptap/extension-underline": "^3.15.3", - "@tiptap/extensions": "^3.15.3", - "@tiptap/pm": "^3.15.3" + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" } }, "node_modules/@types/aria-query": { @@ -4053,28 +2595,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "license": "MIT" - }, - "node_modules/@types/markdown-it": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", - "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", - "license": "MIT", - "dependencies": { - "@types/linkify-it": "^5", - "@types/mdurl": "^2" - } - }, - "node_modules/@types/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "license": "MIT" - }, "node_modules/@types/node": { "version": "25.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", @@ -4089,6 +2609,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -4098,17 +2619,12 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" } }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.50.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz", @@ -4590,6 +3106,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { @@ -4858,6 +3375,29 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4910,12 +3450,6 @@ "integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==", "license": "MIT" }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4971,6 +3505,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, "license": "MIT" }, "node_modules/data-urls": { @@ -5233,6 +3768,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5452,15 +3988,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-equals": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", - "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -5594,21 +4121,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5785,21 +4297,6 @@ "hermes-estree": "0.25.1" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -6087,294 +4584,84 @@ "license": "MIT" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", - "cpu": [ - "arm64" - ], + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + "license": "MIT" }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", - "cpu": [ - "arm64" - ], + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "bin": { + "json5": "lib/cli.js" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=6" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", - "cpu": [ - "x64" - ], + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", - "cpu": [ - "x64" - ], + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/lightningcss-win32-arm64-msvc": { + "node_modules/lightningcss": { "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", - "cpu": [ - "arm64" - ], + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "dev": true, "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "detect-libc": "^2.0.3" + }, "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" } }, "node_modules/lightningcss-win32-x64-msvc": { @@ -6398,21 +4685,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "node_modules/linkifyjs": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", - "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", - "license": "MIT" - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6437,13 +4709,13 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, "node_modules/lucide-react": { @@ -6475,35 +4747,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/markdown-it/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6520,12 +4763,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "license": "MIT" - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -6649,12 +4886,6 @@ "node": ">= 0.8.0" } }, - "node_modules/orderedmap": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", - "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", - "license": "MIT" - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6834,201 +5065,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/prosemirror-changeset": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz", - "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==", - "license": "MIT", - "dependencies": { - "prosemirror-transform": "^1.0.0" - } - }, - "node_modules/prosemirror-collab": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", - "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", - "license": "MIT", - "dependencies": { - "prosemirror-state": "^1.0.0" - } - }, - "node_modules/prosemirror-commands": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", - "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", - "license": "MIT", - "dependencies": { - "prosemirror-model": "^1.0.0", - "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.10.2" - } - }, - "node_modules/prosemirror-dropcursor": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", - "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", - "license": "MIT", - "dependencies": { - "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.1.0", - "prosemirror-view": "^1.1.0" - } - }, - "node_modules/prosemirror-gapcursor": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz", - "integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==", - "license": "MIT", - "dependencies": { - "prosemirror-keymap": "^1.0.0", - "prosemirror-model": "^1.0.0", - "prosemirror-state": "^1.0.0", - "prosemirror-view": "^1.0.0" - } - }, - "node_modules/prosemirror-history": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", - "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", - "license": "MIT", - "dependencies": { - "prosemirror-state": "^1.2.2", - "prosemirror-transform": "^1.0.0", - "prosemirror-view": "^1.31.0", - "rope-sequence": "^1.3.0" - } - }, - "node_modules/prosemirror-inputrules": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", - "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", - "license": "MIT", - "dependencies": { - "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.0.0" - } - }, - "node_modules/prosemirror-keymap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", - "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", - "license": "MIT", - "dependencies": { - "prosemirror-state": "^1.0.0", - "w3c-keyname": "^2.2.0" - } - }, - "node_modules/prosemirror-markdown": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz", - "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==", - "license": "MIT", - "dependencies": { - "@types/markdown-it": "^14.0.0", - "markdown-it": "^14.0.0", - "prosemirror-model": "^1.25.0" - } - }, - "node_modules/prosemirror-menu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz", - "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==", - "license": "MIT", - "dependencies": { - "crelt": "^1.0.0", - "prosemirror-commands": "^1.0.0", - "prosemirror-history": "^1.0.0", - "prosemirror-state": "^1.0.0" - } - }, - "node_modules/prosemirror-model": { - "version": "1.25.4", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", - "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", - "license": "MIT", - "dependencies": { - "orderedmap": "^2.0.0" - } - }, - "node_modules/prosemirror-schema-basic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", - "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", - "license": "MIT", - "dependencies": { - "prosemirror-model": "^1.25.0" - } - }, - "node_modules/prosemirror-schema-list": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", - "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", - "license": "MIT", - "dependencies": { - "prosemirror-model": "^1.0.0", - "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.7.3" - } - }, - "node_modules/prosemirror-state": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", - "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", - "license": "MIT", - "dependencies": { - "prosemirror-model": "^1.0.0", - "prosemirror-transform": "^1.0.0", - "prosemirror-view": "^1.27.0" - } - }, - "node_modules/prosemirror-tables": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", - "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", - "license": "MIT", - "dependencies": { - "prosemirror-keymap": "^1.2.3", - "prosemirror-model": "^1.25.4", - "prosemirror-state": "^1.4.4", - "prosemirror-transform": "^1.10.5", - "prosemirror-view": "^1.41.4" - } - }, - "node_modules/prosemirror-trailing-node": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", - "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", - "license": "MIT", - "dependencies": { - "@remirror/core-constants": "3.0.0", - "escape-string-regexp": "^4.0.0" - }, - "peerDependencies": { - "prosemirror-model": "^1.22.1", - "prosemirror-state": "^1.4.2", - "prosemirror-view": "^1.33.8" - } - }, - "node_modules/prosemirror-transform": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.5.tgz", - "integrity": "sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==", - "license": "MIT", - "dependencies": { - "prosemirror-model": "^1.21.0" - } - }, - "node_modules/prosemirror-view": { - "version": "1.41.5", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.5.tgz", - "integrity": "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==", - "license": "MIT", - "dependencies": { - "prosemirror-model": "^1.20.0", - "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.1.0" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -7045,15 +5081,6 @@ "node": ">=6" } }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/qrcode.react": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", @@ -7105,19 +5132,6 @@ "react": "^19.2.3" } }, - "node_modules/react-google-recaptcha-v3": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/react-google-recaptcha-v3/-/react-google-recaptcha-v3-1.11.0.tgz", - "integrity": "sha512-kLQqpz/77m8+trpBwzqcxNtvWZYoZ/YO6Vm2cVTHW8hs80BWUfDpC7RDwuAvpswwtSYApWfaSpIDFWAIBNIYxQ==", - "license": "MIT", - "dependencies": { - "hoist-non-react-statics": "^3.3.2" - }, - "peerDependencies": { - "react": "^16.3 || ^17.0 || ^18.0 || ^19.0", - "react-dom": "^17.0 || ^18.0 || ^19.0" - } - }, "node_modules/react-hook-form": { "version": "7.69.0", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.69.0.tgz", @@ -7338,12 +5352,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/rope-sequence": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", - "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", - "license": "MIT" - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -7714,12 +5722,6 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "license": "MIT" - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -7982,12 +5984,6 @@ "node": ">=0.10.0" } }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "license": "MIT" - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -8015,7 +6011,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, "license": "MIT", "dependencies": { diff --git a/frontend/src/components/shared/layout/breadcrumbs.tsx b/frontend/src/components/shared/layout/breadcrumbs.tsx index 1fe1f88..e718811 100644 --- a/frontend/src/components/shared/layout/breadcrumbs.tsx +++ b/frontend/src/components/shared/layout/breadcrumbs.tsx @@ -30,10 +30,10 @@ export function Breadcrumbs() { const { t } = useTranslation() const location = useLocation() - // Parse pathname into segments, filtering out 'system' prefix + // Parse pathname into segments const segments = location.pathname .split('/') - .filter((segment) => segment !== '' && segment !== 'system') + .filter((segment) => segment !== '') if (segments.length === 0) { return null @@ -45,7 +45,7 @@ export function Breadcrumbs() { {/* Home link */} - + {t('navigation:breadcrumb.home')} @@ -54,8 +54,7 @@ export function Breadcrumbs() { {segments.map((segment, index) => { const isLast = index === segments.length - 1 - // Prepend /system since we filtered it out from segments - const href = '/system/' + segments.slice(0, index + 1).join('/') + const href = '/' + segments.slice(0, index + 1).join('/') const label = routeLabels[segment] || segment return ( diff --git a/frontend/src/components/shared/layout/mobile-nav.tsx b/frontend/src/components/shared/layout/mobile-nav.tsx index 069af4f..2200158 100644 --- a/frontend/src/components/shared/layout/mobile-nav.tsx +++ b/frontend/src/components/shared/layout/mobile-nav.tsx @@ -37,7 +37,7 @@ export function MobileNav() { setOpen(false)} > diff --git a/frontend/src/components/shared/layout/sidebar.tsx b/frontend/src/components/shared/layout/sidebar.tsx index 3a30d53..c0ddbde 100644 --- a/frontend/src/components/shared/layout/sidebar.tsx +++ b/frontend/src/components/shared/layout/sidebar.tsx @@ -31,7 +31,7 @@ export function Sidebar() { {/* Logo */}
{!isCollapsed && ( - +
E
@@ -41,7 +41,7 @@ export function Sidebar() { )} {isCollapsed && ( - +
E
diff --git a/frontend/src/components/shared/layout/user-menu.tsx b/frontend/src/components/shared/layout/user-menu.tsx index 29e9225..d0bd78f 100644 --- a/frontend/src/components/shared/layout/user-menu.tsx +++ b/frontend/src/components/shared/layout/user-menu.tsx @@ -86,13 +86,13 @@ export function UserMenu() { - + {t('navigation:userMenu.profile')} - + {t('navigation:userMenu.settings')} diff --git a/frontend/src/config/navigation.ts b/frontend/src/config/navigation.ts index b18cbce..1f2b6d3 100644 --- a/frontend/src/config/navigation.ts +++ b/frontend/src/config/navigation.ts @@ -26,32 +26,32 @@ export const navigation: NavSection[] = [ label: 'navigation:sections.system', items: [ { - label: 'navigation:items.systemDashboard', - href: '/system/dashboard', + label: 'navigation:items.dashboard', + href: '/dashboard', icon: LayoutDashboard, // No permission = always visible }, { label: 'navigation:items.users', - href: '/system/users', + href: '/users', icon: Users, permission: 'system:users:read', }, { label: 'navigation:items.auditLogs', - href: '/system/audit-logs', + href: '/audit-logs', icon: FileText, permission: 'system:audit:read', }, { label: 'navigation:items.ipRestrictions', - href: '/system/ip-restrictions', + href: '/ip-restrictions', icon: Shield, permission: 'system:ip-restrictions:read', }, { label: 'navigation:items.email', - href: '/system/email', + href: '/email', icon: Mail, permission: 'email:providers:read', }, @@ -63,7 +63,7 @@ export const navigation: NavSection[] = [ items: [ { label: 'navigation:items.settings', - href: '/system/settings', + href: '/settings', icon: Settings, permission: 'system:settings:read', }, diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx index 7b4367e..e9c19a3 100644 --- a/frontend/src/contexts/auth-context.tsx +++ b/frontend/src/contexts/auth-context.tsx @@ -64,7 +64,7 @@ export function AuthProvider({ children }: AuthProviderProps) { useEffect(() => { const handleSessionExpired = () => { // Redirect immediately to avoid re-render race conditions - window.location.href = '/system/login' + window.location.href = '/login' } const handleForceReauth = () => { @@ -76,7 +76,7 @@ export function AuthProvider({ children }: AuthProviderProps) { }) // Delay redirect so user can see the toast setTimeout(() => { - window.location.href = '/system/login' + window.location.href = '/login' }, 1500) } diff --git a/frontend/src/features/auth/api/auth-api.ts b/frontend/src/features/auth/api/auth-api.ts index 79e6ea7..7303919 100644 --- a/frontend/src/features/auth/api/auth-api.ts +++ b/frontend/src/features/auth/api/auth-api.ts @@ -8,9 +8,6 @@ import type { AcceptInviteRequest, LogoutResponse, InviteValidationDto, - RequestMagicLinkRequest, - RequestMagicLinkResponse, - MagicLinkLoginRequest, } from '../types' export const authApi = { @@ -83,30 +80,4 @@ export const authApi = { ) return extractData(response) }, - - /** - * Request a magic link for passwordless login - */ - requestMagicLink: async ( - request: RequestMagicLinkRequest - ): Promise => { - const response = await apiClient.post>( - '/system/auth/magic-link/request', - request - ) - return extractData(response) - }, - - /** - * Login with magic link token - */ - magicLinkLogin: async ( - request: MagicLinkLoginRequest - ): Promise => { - const response = await apiClient.post>( - '/system/auth/magic-link/login', - request - ) - return extractData(response) - }, } diff --git a/frontend/src/features/auth/components/accept-invite-form.tsx b/frontend/src/features/auth/components/accept-invite-form.tsx index 3592375..04ca623 100644 --- a/frontend/src/features/auth/components/accept-invite-form.tsx +++ b/frontend/src/features/auth/components/accept-invite-form.tsx @@ -81,7 +81,7 @@ export function AcceptInviteForm({ token }: AcceptInviteFormProps) { // Set user in cache BEFORE navigating (triggers isAuthenticated) queryClient.setQueryData(AUTH_QUERY_KEY, pendingAuthResponse.user) localStorage.setItem(AUTH_SESSION_KEY, 'true') - navigate({ to: '/system/dashboard' }) + navigate({ to: '/dashboard' }) } setMfaConfirmOpen(false) } diff --git a/frontend/src/features/auth/components/index.ts b/frontend/src/features/auth/components/index.ts index 7ae4a4b..1da209a 100644 --- a/frontend/src/features/auth/components/index.ts +++ b/frontend/src/features/auth/components/index.ts @@ -2,8 +2,6 @@ export { LoginForm } from './login-form' export { RegisterForm } from './register-form' export { AcceptInviteForm } from './accept-invite-form' export { PasswordRequirements } from './password-requirements' -export { MagicLinkForm } from './magic-link-form' -export { MagicLinkSent } from './magic-link-sent' // MFA components export { MfaSetupModal } from './mfa-setup-modal' diff --git a/frontend/src/features/auth/components/login-form.tsx b/frontend/src/features/auth/components/login-form.tsx index b2fd5c6..7f7196c 100644 --- a/frontend/src/features/auth/components/login-form.tsx +++ b/frontend/src/features/auth/components/login-form.tsx @@ -130,7 +130,7 @@ export function LoginForm() { // Set user in cache BEFORE navigating (triggers isAuthenticated) queryClient.setQueryData(AUTH_QUERY_KEY, pendingAuthResponse.user) localStorage.setItem(AUTH_SESSION_KEY, 'true') - navigate({ to: '/system/dashboard' }) + navigate({ to: '/dashboard' }) } setMfaConfirmOpen(false) } @@ -254,7 +254,7 @@ export function LoginForm() {

{t('auth:login.noAccount')}{' '} - + {t('auth:login.register')}

diff --git a/frontend/src/features/auth/components/magic-link-form.tsx b/frontend/src/features/auth/components/magic-link-form.tsx deleted file mode 100644 index 7db6040..0000000 --- a/frontend/src/features/auth/components/magic-link-form.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useState, useMemo, useCallback } from 'react' -import { useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import { useTranslation } from 'react-i18next' -import { Loader2 } from 'lucide-react' - -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' - -import { useRequestMagicLink } from '../hooks/use-request-magic-link' -import { createMagicLinkSchema, type MagicLinkFormData } from '../types' -import { getErrorMessage } from '@/lib/error-utils' -import { CaptchaWidget } from './captcha-widget' -import { useCaptchaConfig } from '../hooks' - -interface MagicLinkFormProps { - onSuccess?: (email: string) => void - defaultEmail?: string -} - -export function MagicLinkForm({ onSuccess, defaultEmail = '' }: MagicLinkFormProps) { - const { t } = useTranslation() - - // CAPTCHA state - const [captchaToken, setCaptchaToken] = useState(null) - const { data: captchaConfig } = useCaptchaConfig() - const captchaRequired = captchaConfig?.enabled && !!captchaConfig?.siteKey && captchaConfig?.provider !== 'Disabled' - - const handleCaptchaVerify = useCallback((token: string) => { - setCaptchaToken(token) - }, []) - - const handleCaptchaExpire = useCallback(() => { - setCaptchaToken(null) - }, []) - - const { mutate: requestMagicLink, isPending, error } = useRequestMagicLink() - - const magicLinkSchema = useMemo(() => createMagicLinkSchema(t), [t]) - - const form = useForm({ - resolver: zodResolver(magicLinkSchema), - defaultValues: { - email: defaultEmail, - }, - }) - - const onSubmit = (data: MagicLinkFormData) => { - requestMagicLink( - { - email: data.email, - captchaToken: captchaToken || undefined, - }, - { - onSuccess: () => { - if (onSuccess) { - onSuccess(data.email) - } - }, - } - ) - } - - return ( -
- {error && ( -
- {getErrorMessage(error, t)} -
- )} - -
- - - {form.formState.errors.email && ( -

- {form.formState.errors.email.message} -

- )} -
- - {/* CAPTCHA Widget - always visible for magic link */} - - - - - ) -} diff --git a/frontend/src/features/auth/components/magic-link-sent.tsx b/frontend/src/features/auth/components/magic-link-sent.tsx deleted file mode 100644 index 34bb481..0000000 --- a/frontend/src/features/auth/components/magic-link-sent.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useTranslation } from 'react-i18next' -import { Mail } from 'lucide-react' - -import { Button } from '@/components/ui/button' - -interface MagicLinkSentProps { - email: string - onBack: () => void -} - -export function MagicLinkSent({ email, onBack }: MagicLinkSentProps) { - const { t } = useTranslation() - - return ( -
-
-
-
-
- -
-

- {t('auth:magicLink.sent')} -

-

- {t('auth:magicLink.sentMessage', { email })} -

- -
-
-
-
- ) -} diff --git a/frontend/src/features/auth/components/register-form.tsx b/frontend/src/features/auth/components/register-form.tsx index b538aca..42812ff 100644 --- a/frontend/src/features/auth/components/register-form.tsx +++ b/frontend/src/features/auth/components/register-form.tsx @@ -93,7 +93,7 @@ export function RegisterForm() { // Set user in cache BEFORE navigating (triggers isAuthenticated) queryClient.setQueryData(AUTH_QUERY_KEY, pendingAuthResponse.user) localStorage.setItem(AUTH_SESSION_KEY, 'true') - navigate({ to: '/system/dashboard' }) + navigate({ to: '/dashboard' }) } setMfaConfirmOpen(false) } @@ -195,7 +195,7 @@ export function RegisterForm() {

{t('auth:register.hasAccount')}{' '} - + {t('auth:register.signIn')}

diff --git a/frontend/src/features/auth/hooks/index.ts b/frontend/src/features/auth/hooks/index.ts index 29ce035..cc4451b 100644 --- a/frontend/src/features/auth/hooks/index.ts +++ b/frontend/src/features/auth/hooks/index.ts @@ -23,10 +23,6 @@ export { useApproveDeviceFromSession } from './use-approve-device-from-session' export { useForgotPassword } from './use-forgot-password' export { useResetPassword } from './use-reset-password' -// Magic link hooks -export { useRequestMagicLink } from './use-request-magic-link' -export { useMagicLinkLogin, type UseMagicLinkLoginOptions } from './use-magic-link' - // Device approval hooks export { useApproveDeviceByCode } from './use-approve-device-by-code' export { useApproveDeviceByLink } from './use-approve-device-by-link' diff --git a/frontend/src/features/auth/hooks/use-accept-invite.ts b/frontend/src/features/auth/hooks/use-accept-invite.ts index caf846e..da2fca8 100644 --- a/frontend/src/features/auth/hooks/use-accept-invite.ts +++ b/frontend/src/features/auth/hooks/use-accept-invite.ts @@ -28,7 +28,7 @@ export function useAcceptInvite(options?: UseAcceptInviteOptions) { // Update the auth cache with the user data (auto-login) queryClient.setQueryData(AUTH_QUERY_KEY, response.user) // Navigate to dashboard - navigate({ to: '/system/dashboard' }) + navigate({ to: '/dashboard' }) }, }) } diff --git a/frontend/src/features/auth/hooks/use-login.ts b/frontend/src/features/auth/hooks/use-login.ts index 373654f..c684bf3 100644 --- a/frontend/src/features/auth/hooks/use-login.ts +++ b/frontend/src/features/auth/hooks/use-login.ts @@ -43,7 +43,7 @@ export function useLogin(options?: UseLoginOptions) { // Normal login success - set session and navigate localStorage.setItem(AUTH_SESSION_KEY, 'true') queryClient.setQueryData(AUTH_QUERY_KEY, response.user) - navigate({ to: '/system/dashboard' }) + navigate({ to: '/dashboard' }) }, onError: (error) => { const errorCode = (error as { code?: string })?.code?.toLowerCase() diff --git a/frontend/src/features/auth/hooks/use-logout.ts b/frontend/src/features/auth/hooks/use-logout.ts index 02b72b1..37dc506 100644 --- a/frontend/src/features/auth/hooks/use-logout.ts +++ b/frontend/src/features/auth/hooks/use-logout.ts @@ -18,7 +18,7 @@ export function useLogout() { // Clear all cached data queryClient.clear() // Navigate to login - navigate({ to: '/system/login' }) + navigate({ to: '/login' }) }, }) } diff --git a/frontend/src/features/auth/hooks/use-magic-link.ts b/frontend/src/features/auth/hooks/use-magic-link.ts deleted file mode 100644 index 59b893f..0000000 --- a/frontend/src/features/auth/hooks/use-magic-link.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query' -import { useNavigate } from '@tanstack/react-router' -import { authApi } from '../api/auth-api' -import type { MagicLinkLoginRequest, AuthResponse, DeviceApprovalRequiredResponse } from '../types' -import { isDeviceApprovalRequired } from '../types' - -const AUTH_QUERY_KEY = ['auth', 'me'] as const -const AUTH_SESSION_KEY = 'exoauth_has_session' - -export interface UseMagicLinkLoginOptions { - onMfaRequired?: (response: AuthResponse) => void - onMfaSetupRequired?: (response: AuthResponse) => void - onDeviceApprovalRequired?: (response: DeviceApprovalRequiredResponse) => void -} - -export function useMagicLinkLogin(options?: UseMagicLinkLoginOptions) { - const queryClient = useQueryClient() - const navigate = useNavigate() - - return useMutation({ - mutationFn: (data: MagicLinkLoginRequest) => authApi.magicLinkLogin(data), - onSuccess: (response) => { - // Check if device approval is required (risk-based authentication) - if (isDeviceApprovalRequired(response)) { - options?.onDeviceApprovalRequired?.(response) - return - } - - // Check if MFA verification is required - if (response.mfaRequired && response.mfaToken) { - options?.onMfaRequired?.(response) - return - } - - // Check if MFA setup is required (for users with system permissions) - if (response.mfaSetupRequired && response.setupToken) { - options?.onMfaSetupRequired?.(response) - return - } - - // Normal login success - set session and navigate - localStorage.setItem(AUTH_SESSION_KEY, 'true') - queryClient.setQueryData(AUTH_QUERY_KEY, response.user) - navigate({ to: '/system/dashboard' }) - }, - }) -} diff --git a/frontend/src/features/auth/hooks/use-mfa-verify.ts b/frontend/src/features/auth/hooks/use-mfa-verify.ts index e0bc9e5..f63da0b 100644 --- a/frontend/src/features/auth/hooks/use-mfa-verify.ts +++ b/frontend/src/features/auth/hooks/use-mfa-verify.ts @@ -29,7 +29,7 @@ export function useMfaVerify(options?: UseMfaVerifyOptions) { // Normal MFA verify success - set session and navigate localStorage.setItem(AUTH_SESSION_KEY, 'true') queryClient.setQueryData(AUTH_QUERY_KEY, response.user) - navigate({ to: '/system/dashboard' }) + navigate({ to: '/dashboard' }) }, onError: (error) => { const errorCode = (error as { code?: string })?.code?.toLowerCase() diff --git a/frontend/src/features/auth/hooks/use-passkey-login.ts b/frontend/src/features/auth/hooks/use-passkey-login.ts index 0f71570..3432e92 100644 --- a/frontend/src/features/auth/hooks/use-passkey-login.ts +++ b/frontend/src/features/auth/hooks/use-passkey-login.ts @@ -41,7 +41,7 @@ export function usePasskeyLogin(options?: UsePasskeyLoginOptions) { // Normal login success - set session and navigate localStorage.setItem(AUTH_SESSION_KEY, 'true') queryClient.setQueryData(AUTH_QUERY_KEY, response.user) - navigate({ to: '/system/dashboard' }) + navigate({ to: '/dashboard' }) }, }) } diff --git a/frontend/src/features/auth/hooks/use-register.ts b/frontend/src/features/auth/hooks/use-register.ts index 489c383..d878b0c 100644 --- a/frontend/src/features/auth/hooks/use-register.ts +++ b/frontend/src/features/auth/hooks/use-register.ts @@ -26,7 +26,7 @@ export function useRegister(options?: UseRegisterOptions) { // Normal register success - set session and navigate localStorage.setItem(AUTH_SESSION_KEY, 'true') queryClient.setQueryData(AUTH_QUERY_KEY, response.user) - navigate({ to: '/system/dashboard' }) + navigate({ to: '/dashboard' }) }, }) } diff --git a/frontend/src/features/auth/hooks/use-request-magic-link.ts b/frontend/src/features/auth/hooks/use-request-magic-link.ts deleted file mode 100644 index 0449027..0000000 --- a/frontend/src/features/auth/hooks/use-request-magic-link.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useMutation } from '@tanstack/react-query' -import { authApi } from '../api/auth-api' -import type { RequestMagicLinkRequest } from '@/types/auth' - -export function useRequestMagicLink() { - return useMutation({ - mutationFn: (request: RequestMagicLinkRequest) => - authApi.requestMagicLink(request), - }) -} diff --git a/frontend/src/features/auth/index.ts b/frontend/src/features/auth/index.ts index 07c40a5..1ce1c2d 100644 --- a/frontend/src/features/auth/index.ts +++ b/frontend/src/features/auth/index.ts @@ -1,12 +1,5 @@ // Components -export { - LoginForm, - RegisterForm, - AcceptInviteForm, - PasswordRequirements, - MagicLinkForm, - MagicLinkSent, -} from './components' +export { LoginForm, RegisterForm, AcceptInviteForm, PasswordRequirements } from './components' // Hooks export { diff --git a/frontend/src/features/auth/types/index.ts b/frontend/src/features/auth/types/index.ts index 6d32dbb..6dd2cbb 100644 --- a/frontend/src/features/auth/types/index.ts +++ b/frontend/src/features/auth/types/index.ts @@ -8,7 +8,6 @@ export type { LogoutResponse, ForgotPasswordResponse, ResetPasswordResponse, - RequestMagicLinkResponse, DeviceInfo, LoginRequest, RegisterRequest, @@ -16,8 +15,6 @@ export type { RefreshTokenRequest, ForgotPasswordRequest, ResetPasswordRequest, - RequestMagicLinkRequest, - MagicLinkLoginRequest, MfaVerifyRequest, SessionInfo, } from '@/types/auth' @@ -83,15 +80,6 @@ export interface AcceptInviteFormData { confirmPassword: string } -export interface MagicLinkFormData { - email: string -} - -export const createMagicLinkSchema = (t: TFunction) => - z.object({ - email: z.string().email(t('validation:email')), - }) - // Invite validation types (public endpoint) export interface InviterDto { fullName: string diff --git a/frontend/src/i18n/locales/de/auth.json b/frontend/src/i18n/locales/de/auth.json index 22317e8..109a2bd 100644 --- a/frontend/src/i18n/locales/de/auth.json +++ b/frontend/src/i18n/locales/de/auth.json @@ -253,29 +253,9 @@ "loading": "Sicherheitsprüfung wird geladen...", "loadError": "Sicherheitsprüfung konnte nicht geladen werden. Bitte aktualisieren Sie die Seite.", "verificationError": "Verifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.", - "error": "Sicherheitsprüfung fehlgeschlagen. Bitte versuchen Sie es erneut.", "disabled": "Sicherheitsprüfung nicht erforderlich", "recaptcha": { "indicator": "Geschützt durch reCAPTCHA" } - }, - "magicLink": { - "title": "Mit Magic Link anmelden", - "email": "E-Mail", - "button": "Magic Link senden", - "sending": "Wird gesendet...", - "sent": "Überprüfen Sie Ihre E-Mails!", - "sentMessage": "Wir haben einen Magic Link an {{email}} gesendet. Klicken Sie auf den Link in der E-Mail, um sich anzumelden.", - "checkEmail": "Überprüfen Sie Ihre E-Mails für den Magic Link", - "validating": "Link wird validiert...", - "validatingMessage": "Bitte warten Sie, während wir Ihren Magic Link verifizieren", - "error": "Verifizierung fehlgeschlagen", - "invalidToken": "Dieser Magic Link ist ungültig oder wurde bereits verwendet", - "expired": "Dieser Magic Link ist abgelaufen", - "retry": "Erneut versuchen", - "retrying": "Wird erneut versucht...", - "retryOptions": "Sie können es mit den folgenden Optionen erneut versuchen:", - "success": "Erfolgreich angemeldet!", - "redirecting": "Weiterleitung zum Dashboard..." } } diff --git a/frontend/src/i18n/locales/de/navigation.json b/frontend/src/i18n/locales/de/navigation.json index 4408dad..3f9a475 100644 --- a/frontend/src/i18n/locales/de/navigation.json +++ b/frontend/src/i18n/locales/de/navigation.json @@ -7,7 +7,6 @@ }, "items": { "dashboard": "Dashboard", - "systemDashboard": "System-Dashboard", "users": "Benutzer", "permissions": "Berechtigungen", "auditLogs": "Audit-Protokolle", @@ -20,7 +19,6 @@ "breadcrumb": { "home": "Startseite", "dashboard": "Dashboard", - "systemDashboard": "System-Dashboard", "users": "Benutzer", "userDetails": "Benutzerdetails", "createUser": "Benutzer erstellen", diff --git a/frontend/src/i18n/locales/en/auth.json b/frontend/src/i18n/locales/en/auth.json index d2b41f0..4f6755f 100644 --- a/frontend/src/i18n/locales/en/auth.json +++ b/frontend/src/i18n/locales/en/auth.json @@ -10,9 +10,7 @@ "signingIn": "Signing in...", "noAccount": "Don't have an account?", "register": "Register", - "orContinueWith": "or continue with email", - "useMagicLink": "Sign in with magic link", - "usePassword": "Sign in with password" + "orContinueWith": "or continue with email" }, "register": { "title": "Create Account", @@ -255,29 +253,9 @@ "loading": "Loading security verification...", "loadError": "Failed to load security verification. Please refresh the page.", "verificationError": "Verification failed. Please try again.", - "error": "Security verification failed. Please try again.", "disabled": "Security verification not required", "recaptcha": { "indicator": "Protected by reCAPTCHA" } - }, - "magicLink": { - "title": "Sign In with Magic Link", - "email": "Email", - "button": "Send Magic Link", - "sending": "Sending...", - "sent": "Check your email!", - "sentMessage": "We've sent a magic link to {{email}}. Click the link in the email to sign in.", - "checkEmail": "Check your email for the magic link", - "validating": "Validating link...", - "validatingMessage": "Please wait while we verify your magic link", - "error": "Verification Failed", - "invalidToken": "This magic link is invalid or has already been used", - "expired": "This magic link has expired", - "retry": "Try Again", - "retrying": "Retrying...", - "retryOptions": "You can try again with the following options:", - "success": "Signed in successfully!", - "redirecting": "Redirecting to dashboard..." } } diff --git a/frontend/src/i18n/locales/en/errors.json b/frontend/src/i18n/locales/en/errors.json index 409f915..17fb0c0 100644 --- a/frontend/src/i18n/locales/en/errors.json +++ b/frontend/src/i18n/locales/en/errors.json @@ -13,8 +13,6 @@ "AUTH_FORCE_REAUTH": "Your session has been invalidated. Please sign in again.", "AUTH_INVITE_EXPIRED": "Invitation expired. Please request a new one.", "AUTH_INVITE_INVALID": "Invalid invitation link", - "AUTH_MAGIC_LINK_INVALID": "Invalid or missing magic link token", - "AUTH_MAGIC_LINK_EXPIRED": "Magic link has expired. Please request a new one.", "PASSWORD_RESET_TOKEN_INVALID": "Invalid password reset link. Please request a new one.", "PASSWORD_RESET_TOKEN_EXPIRED": "Password reset link has expired. Please request a new one.", "AUTH_USER_NOT_FOUND": "No account found with this email address.", diff --git a/frontend/src/i18n/locales/en/navigation.json b/frontend/src/i18n/locales/en/navigation.json index 89d1822..8dec70c 100644 --- a/frontend/src/i18n/locales/en/navigation.json +++ b/frontend/src/i18n/locales/en/navigation.json @@ -7,7 +7,6 @@ }, "items": { "dashboard": "Dashboard", - "systemDashboard": "System Dashboard", "users": "Users", "permissions": "Permissions", "auditLogs": "Audit Logs", @@ -20,7 +19,6 @@ "breadcrumb": { "home": "Home", "dashboard": "Dashboard", - "systemDashboard": "System Dashboard", "users": "Users", "userDetails": "User Details", "createUser": "Create User", diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index ae66019..9413125 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -18,14 +18,13 @@ import { ImprintPage, PrivacyPage, TermsPage } from './legal' import { ResetPasswordPage } from './reset-password' import { ApproveDevicePage } from './approve-device' import { EmailPage } from './email' -import { MagicLinkLoginPage } from './magic-link-login' // Permission-protected page wrapper function withPermission(Component: React.ComponentType, permission: string) { return function ProtectedPage() { const { hasPermission } = usePermissions() if (!hasPermission(permission)) { - return + return } return } @@ -51,28 +50,28 @@ const authLayoutRoute = createRoute({ // Login route const loginRoute = createRoute({ getParentRoute: () => authLayoutRoute, - path: '/system/login', + path: '/login', component: LoginPage, }) // Register route const registerRoute = createRoute({ getParentRoute: () => authLayoutRoute, - path: '/system/register', + path: '/register', component: RegisterPage, }) // Forbidden route const forbiddenRoute = createRoute({ getParentRoute: () => authLayoutRoute, - path: '/system/forbidden', + path: '/forbidden', component: ForbiddenPage, }) // Invite route - for accepting invitations const inviteRoute = createRoute({ getParentRoute: () => authLayoutRoute, - path: '/system/invite', + path: '/invite', component: InvitePage, validateSearch: (search: Record) => ({ token: (search.token as string) || '', @@ -101,7 +100,7 @@ const termsRoute = createRoute({ // Reset Password route const resetPasswordRoute = createRoute({ getParentRoute: () => authLayoutRoute, - path: '/system/reset-password', + path: '/reset-password', component: ResetPasswordPage, validateSearch: (search: Record) => ({ token: (search.token as string) || '', @@ -111,20 +110,10 @@ const resetPasswordRoute = createRoute({ // Approve Device route (email link approval) const approveDeviceRoute = createRoute({ getParentRoute: () => authLayoutRoute, - path: '/system/approve-device/$token', + path: '/approve-device/$token', component: ApproveDevicePage, }) -// Magic Link Login route -const magicLinkLoginRoute = createRoute({ - getParentRoute: () => authLayoutRoute, - path: '/system/magic-link-login', - component: MagicLinkLoginPage, - validateSearch: (search: Record) => ({ - token: (search.token as string) || '', - }), -}) - // App layout - for authenticated pages (with sidebar) const appLayoutRoute = createRoute({ getParentRoute: () => rootRoute, @@ -144,7 +133,7 @@ function AppLayoutWrapper() { } if (!isAuthenticated) { - return + return } return ( @@ -161,45 +150,45 @@ const indexRoute = createRoute({ component: IndexRedirect, }) -// System Dashboard route +// Dashboard route const dashboardRoute = createRoute({ getParentRoute: () => appLayoutRoute, - path: '/system/dashboard', + path: '/dashboard', component: DashboardPage, }) // Users route (requires system:users:read) const usersRoute = createRoute({ getParentRoute: () => appLayoutRoute, - path: '/system/users', + path: '/users', component: withPermission(UsersPage, 'system:users:read'), }) // Audit Logs route (requires system:audit:read) const auditLogsRoute = createRoute({ getParentRoute: () => appLayoutRoute, - path: '/system/audit-logs', + path: '/audit-logs', component: withPermission(AuditLogsPage, 'system:audit:read'), }) // IP Restrictions route (requires system:ip-restrictions:read) const ipRestrictionsRoute = createRoute({ getParentRoute: () => appLayoutRoute, - path: '/system/ip-restrictions', + path: '/ip-restrictions', component: withPermission(IpRestrictionsPage, 'system:ip-restrictions:read'), }) // Email route (requires any email permission - tabs check individual permissions) const emailRoute = createRoute({ getParentRoute: () => appLayoutRoute, - path: '/system/email', + path: '/email', component: EmailPage, }) // Settings route const settingsRoute = createRoute({ getParentRoute: () => appLayoutRoute, - path: '/system/settings', + path: '/settings', component: SettingsPage, }) @@ -216,7 +205,6 @@ export const routeTree = rootRoute.addChildren([ termsRoute, resetPasswordRoute, approveDeviceRoute, - magicLinkLoginRoute, ]), appLayoutRoute.addChildren([ dashboardRoute, diff --git a/frontend/src/routes/approve-device.tsx b/frontend/src/routes/approve-device.tsx index fa689e5..090ba72 100644 --- a/frontend/src/routes/approve-device.tsx +++ b/frontend/src/routes/approve-device.tsx @@ -30,7 +30,7 @@ export function ApproveDevicePage() { {t('auth:deviceApproval.linkApproval.error')}

@@ -79,7 +79,7 @@ export function ApproveDevicePage() { {getErrorMessage(error, t) || t('auth:deviceApproval.linkApproval.error')}

@@ -107,7 +107,7 @@ export function ApproveDevicePage() { {t('auth:deviceApproval.linkApproval.success')}

diff --git a/frontend/src/routes/forbidden.tsx b/frontend/src/routes/forbidden.tsx index 30e04c7..e4603d4 100644 --- a/frontend/src/routes/forbidden.tsx +++ b/frontend/src/routes/forbidden.tsx @@ -23,7 +23,7 @@ export function ForbiddenPage() {

- {loginMode === 'password' ? : } - -
- -
+
diff --git a/frontend/src/routes/magic-link-login.tsx b/frontend/src/routes/magic-link-login.tsx deleted file mode 100644 index 317f719..0000000 --- a/frontend/src/routes/magic-link-login.tsx +++ /dev/null @@ -1,273 +0,0 @@ -import { useState, useEffect } from 'react' -import { useTranslation } from 'react-i18next' -import { Link, useSearch, useNavigate } from '@tanstack/react-router' -import { useQueryClient } from '@tanstack/react-query' -import { Loader2, AlertTriangle } from 'lucide-react' - -import { Button } from '@/components/ui/button' -import { Checkbox } from '@/components/ui/checkbox' -import { Label } from '@/components/ui/label' -import { useMagicLinkLogin, type UseMagicLinkLoginOptions } from '@/features/auth/hooks' -import { getErrorMessage } from '@/lib/error-utils' -import { getDeviceInfo } from '@/lib/device' -import { MfaVerifyModal } from '@/features/auth/components/mfa-verify-modal' -import { MfaSetupModal } from '@/features/auth/components/mfa-setup-modal' -import { MfaConfirmModal } from '@/features/auth/components/mfa-confirm-modal' -import { DeviceApprovalModal } from '@/features/auth/components/device-approval-modal' -import type { AuthResponse } from '@/types/auth' -import type { MfaConfirmResponse, DeviceApprovalRequiredResponse } from '@/features/auth/types' - -const AUTH_SESSION_KEY = 'exoauth_has_session' -const AUTH_QUERY_KEY = ['auth', 'me'] as const - -export function MagicLinkLoginPage() { - const { t } = useTranslation() - const search = useSearch({ strict: false }) as { token?: string } - const token = search.token || '' - const navigate = useNavigate() - const queryClient = useQueryClient() - - const [rememberMe, setRememberMe] = useState(false) - - // MFA state - const [mfaVerifyOpen, setMfaVerifyOpen] = useState(false) - const [mfaSetupOpen, setMfaSetupOpen] = useState(false) - const [mfaConfirmOpen, setMfaConfirmOpen] = useState(false) - const [mfaToken, setMfaToken] = useState(null) - const [setupToken, setSetupToken] = useState(null) - const [backupCodes, setBackupCodes] = useState([]) - const [pendingAuthResponse, setPendingAuthResponse] = useState(null) - - // Device approval state - const [deviceApprovalOpen, setDeviceApprovalOpen] = useState(false) - const [deviceApprovalToken, setDeviceApprovalToken] = useState(null) - const [deviceRiskFactors, setDeviceRiskFactors] = useState([]) - - const options: UseMagicLinkLoginOptions = { - onMfaRequired: (response: AuthResponse) => { - setMfaToken(response.mfaToken) - setMfaVerifyOpen(true) - }, - onMfaSetupRequired: (response: AuthResponse) => { - setSetupToken(response.setupToken) - setMfaSetupOpen(true) - }, - onDeviceApprovalRequired: (response: DeviceApprovalRequiredResponse) => { - setDeviceApprovalToken(response.approvalToken) - setDeviceRiskFactors(response.riskFactors) - setDeviceApprovalOpen(true) - }, - } - - const magicLinkLogin = useMagicLinkLogin(options) - - // Automatically trigger login when token is available - useEffect(() => { - if (token && !magicLinkLogin.isPending && !magicLinkLogin.isSuccess && !magicLinkLogin.isError) { - const deviceInfo = getDeviceInfo() - magicLinkLogin.mutate({ - token, - rememberMe, - ...deviceInfo, - }) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [token]) - - const handleMfaSetupSuccess = (response: MfaConfirmResponse) => { - setBackupCodes(response.backupCodes) - setPendingAuthResponse(response) - setMfaSetupOpen(false) - setMfaConfirmOpen(true) - } - - const handleMfaConfirmContinue = () => { - // If we have auth data from setupToken flow, complete the login - if (pendingAuthResponse?.accessToken && pendingAuthResponse.user) { - // Set user in cache BEFORE navigating (triggers isAuthenticated) - queryClient.setQueryData(AUTH_QUERY_KEY, pendingAuthResponse.user) - localStorage.setItem(AUTH_SESSION_KEY, 'true') - navigate({ to: '/system/dashboard' }) - } - setMfaConfirmOpen(false) - } - - // No token provided - if (!token) { - return ( -
-
-
-
-
- -
-

- {t('errors:api.badRequest')} -

-

- {t('errors:codes.AUTH_MAGIC_LINK_INVALID')} -

- -
-
-
-
- ) - } - - // Loading state (validating token) - if (magicLinkLogin.isPending) { - return ( -
-
-
-
- -

- {t('auth:magicLink.validating')} -

-

- {t('auth:magicLink.validatingMessage')} -

-
-
-
-
- ) - } - - // Error state - if (magicLinkLogin.isError) { - return ( -
-
-
-

- {t('auth:magicLink.error')} -

-
- -
-
-
- {getErrorMessage(magicLinkLogin.error, t)} -
- -
- -
- setRememberMe(checked === true)} - /> - -
-
- - - - -
-
-
-
- ) - } - - // Success state is handled by the hook navigating to /system/dashboard - // This return should not be reached, but included for completeness - return ( -
-
-
-
- -

- {t('auth:magicLink.success')} -

-

- {t('auth:magicLink.redirecting')} -

-
-
-
- - {/* MFA Verify Modal - shown when user has MFA enabled */} - { - setMfaVerifyOpen(false) - setDeviceApprovalToken(response.approvalToken) - setDeviceRiskFactors(response.riskFactors) - setDeviceApprovalOpen(true) - }} - /> - - {/* MFA Setup Modal - shown when MFA is required but not set up */} - - - {/* MFA Confirm Modal - shows backup codes after setup */} - - - {/* Device Approval Modal - shown when risk-based auth requires device verification */} - { - // After device approval, user needs to login again - setDeviceApprovalToken(null) - setDeviceRiskFactors([]) - }} - /> -
- ) -} diff --git a/frontend/src/routes/not-found.tsx b/frontend/src/routes/not-found.tsx index 5a9e920..37caa20 100644 --- a/frontend/src/routes/not-found.tsx +++ b/frontend/src/routes/not-found.tsx @@ -23,7 +23,7 @@ export function NotFoundPage() {

@@ -113,7 +113,7 @@ export function ResetPasswordPage() { {t('auth:resetPassword.successMessage')}

@@ -190,7 +190,7 @@ export function ResetPasswordPage() {

- + {t('auth:forgotPassword.backToLogin')}

diff --git a/frontend/src/routes/server-error.tsx b/frontend/src/routes/server-error.tsx index dd95fdd..ba48511 100644 --- a/frontend/src/routes/server-error.tsx +++ b/frontend/src/routes/server-error.tsx @@ -30,7 +30,7 @@ export function ServerErrorPage() { {t('general.retry')}