diff --git a/README.md b/README.md index 69aad5de..d1c63fa8 100644 --- a/README.md +++ b/README.md @@ -16,38 +16,118 @@ The first preview release (**v 0.1.0-preview**) is planned within the next week. [![Discord](https://img.shields.io/discord/1459498792192839774?color=%237289da&label=Discord&logo=discord&logoColor=%237289da&style=flat-square)](https://discord.gg/QscA86dXSR) [![codecov](https://codecov.io/gh/CodeBeamOrg/UltimateAuth/branch/dev/graph/badge.svg)](https://codecov.io/gh/CodeBeamOrg/UltimateAuth) +--- + +## 📑 Table of Contents + +- [🗺 Roadmap](#-roadmap) +- [🌟 Why UltimateAuth](#-why-ultimateauth) +- [🚀 Quick Start](#-quick-start) +- [💡 Usage](#-usage) +- [📘 Documentation](#-documentation) +- [🤝 Contributing](#-contributing) +- [⭐ Acknowledgements](#-acknowledgements) --- -UltimateAuth is an open-source authentication framework that unifies secure session and token based authentication, modern PKCE flows, Blazor/Maui-ready client experiences, and a fully extensible architecture — all with a focus on clarity, lightweight design, and developer happiness. +UltimateAuth is an open-source auth framework with platform-level capabilities that unifies secure session, cookie and token based Auth, modern PKCE flows, Blazor/Maui-ready client experiences - eliminating the complexity of traditional Auth systems while providing a clean, lightweight, extensible and developer-first architecture. + +--- +## 🗺 Roadmap + +| Phase | Version | Scope | Status | Release Date | +| ----------------------- | ------------- | ----------------------------------------- | -------------- | ------------ | +| First Preview | 0.1.0-preview | "Stable" Preview Core | ✅ Completed | Last check | +| First Release* | 0.1.0 | Fully Documented & Quality Tested | 🟡 In Progress | Q2 2026 | +| Product Expansion | 0.2.0 | Full Auth Modes | 🟡 In Progress | Q2 2026 | +| Security Expansion | 0.3.0 | MFA, Reauth, Rate Limiting | 🔜 Planned | Q2 2026 | +| Infrastructure Expansion| 0.4.0 | Redis, Distributed Cache, Password Hasher | 🔜 Planned | Q2 2026 | +| Multi-Tenant Expansion | 0.5.0 | Multi tenant management | 🔜 Planned | Q3 2026 | +| Extensibility Expansion | 0.6.0 | Audit, events, hooks | 🔜 Planned | Q3 2026 | +| Performance Expansion | 0.7.0 | Benchmarks, caching | 🔜 Planned | Q3 2026 | +| Ecosystem Expansion | 0.8.0 | Migration tools | 🔜 Planned | Q4 2026 | +| v1.0 | 1.0.0 | Locked API, align with .NET 11 | 🔜 Planned | Q4 2026 | + +*v 0.1.0 already provides a skeleton of multi tenancy, MFA, reauth etc. Expansion releases will enhance these areas. + +> The project roadmap is actively maintained as a GitHub issue: + +👉 https://github.com/CodeBeamOrg/UltimateAuth/issues/8 + +We keep it up-to-date with current priorities, planned features, and progress. Feel free to follow, comment, or contribute ideas. + +
+ +> UltimateAuth is currently in the final stage of the first preview release (v 0.1.0-preview). + +> Core architecture is complete and validated through working samples. + +> Ongoing work: +> - Final API surface review +> - Developer experience improvements +> - EF Core integration polishing +> - Documentation refinement +
--- ## 🌟 Why UltimateAuth: The Six-Point Principles -### **1) Developer-Centric & User-Friendly** -Clean APIs, predictable behavior, minimal ceremony — designed to make authentication *pleasant* for developers. +### 1) Unified Authentication System + +One solution, one mental model — across Blazor Server, WASM, MAUI, and APIs. +UltimateAuth eliminates fragmentation by handling client differences internally and exposing a single, consistent API. -### **2) Security-Driven** -PKCE, hardened session flows, reuse detection, event-driven safeguards, device awareness, and modern best practices. +### 2) Plug & Play Ready -### **3) Extensible & Lightweight by Design** -Every component can be replaced or overridden. -No forced dependencies. No unnecessary weight. +Built-in capabilities designed for real-world scenarios: -### **4) Plug-and-Play Ready** -From setup to production, UltimateAuth prioritizes a frictionless integration journey with sensible defaults. +- Automatic client profile detection (blazor server - WASM - MAUI) +- Selectable authentication modes (Session / Token / Hybrid / SemiHybrid) +- Device-aware sessions +- PKCE flows out of the box +- Unified session + token lifecycle +- Event-driven extensibility -### **5) Blazor & MAUI-Ready for Modern .NET** -Blazor WebApp, Blazor WASM, Blazor Server, and .NET MAUI expose weaknesses in traditional auth systems. -UltimateAuth is engineered from day one to support real-world scenarios across the entire modern .NET UI stack. +No boilerplate. No hidden complexity. -### **6) Unified Framework** -One solution, same codebase across Blazor server, WASM and MAUI. UltimateAuth handles client differences internally and providing consistent and reliable public API. +### 3) Developer-Centric + +Clean APIs, predictable behavior, minimal ceremony — designed to make authentication pleasant. + +### 4) Security as a First-Class Concern + +Modern security built-in by default: + +- PKCE support +- Session reuse detection +- Device tracking +- Hardened auth flows +- Safe defaults with extensibility + +### 5) Extensible & Lightweight + +Start simple, scale infinitely: + +- Works out of the box with sensible defaults +- Replace any component when needed +- No forced architecture decisions + +### 6) Built for Modern .NET Applications + +Designed specifically for real-world .NET environments: + +- Blazor Server +- Blazor WASM +- .NET MAUI +- Backend APIs + +Traditional auth solutions struggle here — UltimateAuth embraces it. --- # 🚀 Quick Start +> ⏱ Takes ~2 minutes to get started ### 1) Install packages (Will be available soon) @@ -66,7 +146,11 @@ Server registration: ```csharp builder.Services .AddUltimateAuthServer() - .AddUltimateAuthEntityFrameworkCore(); // Production + .AddUltimateAuthEntityFrameworkCore(db => + { + // use with your database provider + db.UseSqlite("Data Source=uauth.db"); + }); // OR @@ -107,7 +191,36 @@ Place this in `App.razor` or `index.html` ``` -### 5) Optional: Blazor Usings +### 5) 🗄️ Database Setup (EF Core) + +After configuring UltimateAuth with Entity Framework Core, you need to create and apply database migrations. + +5.1) Install EF Core tools (if not installed) +```bash +dotnet tool install --global dotnet-ef +``` +5.2) Add migration +```bash +dotnet ef migrations add InitUAuth +``` + +5.3) Update database +```bash +dotnet ef database update +``` +💡 Visual Studio (PMC alternative) + +If you are using Visual Studio, you can run these commands in Package Manager Console: +```bash +Add-Migration InitUAuth -Context UAuthDbContext +Update-Database -Context UAuthDbContext +``` +⚠️ Notes +- Migrations must be created in your application project, not in the UltimateAuth packages +- You are responsible for managing migrations in production +- Automatic database initialization is not enabled by default + +### 6) Optional: Blazor Usings Add this in `_Imports.razor` ```csharp @using CodeBeam.UltimateAuth.Client.Blazor @@ -181,39 +294,6 @@ UltimateAuth turns Auth into a simple application service — not a separate sys --- - -## 📅 Release Timeline (Targeted) - -> _Dates reflect targeted milestones and may evolve with community feedback._ - -### **Q1 2026 — First Release** -- v 0.1.0-preview to v 0.1.0 - -### **Q2 2026 — Stable Feature Releases** -- v 0.2.0 to v 0.3.0 - -### **Q3 2026 — General Availability** -- API surface locked -- Production-ready security hardening -- Unified architecture finalized - -### **Q4 2026 — v 11.x.x (.NET 11 Alignment Release)** -UltimateAuth adopts .NET platform versioning to align with the broader ecosystem. - ---- - -## 🗺 Roadmap - -The project roadmap is actively maintained as a GitHub issue: - -👉 https://github.com/CodeBeamOrg/UltimateAuth/issues/8 - -We keep it up-to-date with current priorities, planned features, and progress. - -Feel free to follow, comment, or contribute ideas. - ---- - ## 📘 Documentation Two documentation experiences will be provided: @@ -235,22 +315,6 @@ Discussions are open — your ideas matter. --- -## 🛠 Project Status - -UltimateAuth core architecture is implemented and validated through the sample application. - -We are currently: - -- Polishing developer experience -- Reviewing public APIs -- Preparing EF Core integration packages - -Preview release is coming soon. - -You can check the samples and try what UltimateAuth offers by downloading repo and running locally. - ---- - ## ⭐ Acknowledgements UltimateAuth is built with love by CodeBeam and shaped by real-world .NET development — diff --git a/UltimateAuth.slnx b/UltimateAuth.slnx index be71aa02..cba5a257 100644 --- a/UltimateAuth.slnx +++ b/UltimateAuth.slnx @@ -1,12 +1,14 @@ - + + + diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCoreReference.csproj b/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj similarity index 100% rename from nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCoreReference.csproj rename to nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/CodeBeam.UltimateAuth.EntityFrameworkCore.Bundle.csproj diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Data/UAuthDbContext.cs b/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Data/UAuthDbContext.cs new file mode 100644 index 00000000..35347918 --- /dev/null +++ b/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Data/UAuthDbContext.cs @@ -0,0 +1,53 @@ +using CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Users.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.EntityFrameworkCore; + +public sealed class UAuthDbContext : DbContext +{ + public UAuthDbContext(DbContextOptions options) + : base(options) + { + } + + // Users + public DbSet UserLifecycles => Set(); + public DbSet UserProfiles => Set(); + public DbSet UserIdentifiers => Set(); + + // Credentials + public DbSet PasswordCredentials => Set(); + + // Authorization + public DbSet Roles => Set(); + public DbSet UserRoleAssignments => Set(); + public DbSet UserPermissions => Set(); + + // Sessions + public DbSet Roots => Set(); + public DbSet Chains => Set(); + public DbSet Sessions => Set(); + + // Tokens + public DbSet RefreshTokens => Set(); + + // Authentication + public DbSet AuthenticationSecurityStates => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + UAuthSessionsModelBuilder.Configure(modelBuilder); + UAuthTokensModelBuilder.Configure(modelBuilder); + UAuthAuthenticationModelBuilder.Configure(modelBuilder); + UAuthUsersModelBuilder.Configure(modelBuilder); + UAuthCredentialsModelBuilder.Configure(modelBuilder); + UAuthAuthorizationModelBuilder.Configure(modelBuilder); + } +} diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/UltimateAuthEntityFrameworkCoreExtensions.cs b/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Extensions/UltimateAuthEntityFrameworkCoreExtensions.cs similarity index 68% rename from nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/UltimateAuthEntityFrameworkCoreExtensions.cs rename to nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Extensions/UltimateAuthEntityFrameworkCoreExtensions.cs index f5efa97b..6d69104d 100644 --- a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/UltimateAuthEntityFrameworkCoreExtensions.cs +++ b/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Extensions/UltimateAuthEntityFrameworkCoreExtensions.cs @@ -1,9 +1,15 @@ -using CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.Extensions; +using CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.Extensions; +using CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; using CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.Extensions; +using CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; using CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.Extensions; using CodeBeam.UltimateAuth.Reference.Bundle; +using CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; using CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.Extensions; +using CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; using CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.Extensions; +using CodeBeam.UltimateAuth.Users.EntityFrameworkCore; using CodeBeam.UltimateAuth.Users.EntityFrameworkCore.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -56,14 +62,9 @@ public static class UltimateAuthEntityFrameworkCoreExtensions /// public static IServiceCollection AddUltimateAuthEntityFrameworkCore(this IServiceCollection services, Action configureDb) { - services - .AddUltimateAuthReferences() - .AddUltimateAuthUsersEntityFrameworkCore(configureDb) - .AddUltimateAuthCredentialsEntityFrameworkCore(configureDb) - .AddUltimateAuthAuthorizationEntityFrameworkCore(configureDb) - .AddUltimateAuthSessionsEntityFrameworkCore(configureDb) - .AddUltimateAuthTokensEntityFrameworkCore(configureDb) - .AddUltimateAuthAuthenticationEntityFrameworkCore(configureDb); + services.AddUltimateAuthReferences(); + services.AddDbContext(configureDb); + services.AddUltimateAuthEfCoreStores(); return services; } @@ -91,13 +92,24 @@ public static IServiceCollection AddUltimateAuthEntityFrameworkCore(this IServic services .AddUltimateAuthReferences() - .AddUltimateAuthUsersEntityFrameworkCore(options.Resolve(options.Users)) - .AddUltimateAuthCredentialsEntityFrameworkCore(options.Resolve(options.Credentials)) - .AddUltimateAuthAuthorizationEntityFrameworkCore(options.Resolve(options.Authorization)) - .AddUltimateAuthSessionsEntityFrameworkCore(options.Resolve(options.Sessions)) - .AddUltimateAuthTokensEntityFrameworkCore(options.Resolve(options.Tokens)) - .AddUltimateAuthAuthenticationEntityFrameworkCore(options.Resolve(options.Authentication)); + .AddUltimateAuthUsersEntityFrameworkCore(options.Resolve(options.Users)) + .AddUltimateAuthCredentialsEntityFrameworkCore(options.Resolve(options.Credentials)) + .AddUltimateAuthAuthorizationEntityFrameworkCore(options.Resolve(options.Authorization)) + .AddUltimateAuthSessionsEntityFrameworkCore(options.Resolve(options.Sessions)) + .AddUltimateAuthTokensEntityFrameworkCore(options.Resolve(options.Tokens)) + .AddUltimateAuthAuthenticationEntityFrameworkCore(options.Resolve(options.Authentication)); return services; } + + public static IServiceCollection AddUltimateAuthEfCoreStores(this IServiceCollection services) + { + return services + .AddUltimateAuthUsersEntityFrameworkCore() + .AddUltimateAuthSessionsEntityFrameworkCore() + .AddUltimateAuthTokensEntityFrameworkCore() + .AddUltimateAuthAuthorizationEntityFrameworkCore() + .AddUltimateAuthCredentialsEntityFrameworkCore() + .AddUltimateAuthAuthenticationEntityFrameworkCore(); + } } diff --git a/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/UAuthEfCoreOptions.cs b/nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Options/UAuthEfCoreOptions.cs similarity index 100% rename from nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/UAuthEfCoreOptions.cs rename to nuget/CodeBeam.UltimateAuth.EntityFrameworkCore/Options/UAuthEfCoreOptions.cs diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs b/samples/CodeBeam.UltimateAuth.Sample.Seed/AuthorizationSeedContributor.cs similarity index 86% rename from src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs rename to samples/CodeBeam.UltimateAuth.Sample.Seed/AuthorizationSeedContributor.cs index e9e22abf..da5c0ece 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/InMemoryAuthorizationSeedContributor.cs +++ b/samples/CodeBeam.UltimateAuth.Sample.Seed/AuthorizationSeedContributor.cs @@ -1,24 +1,24 @@ -using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Authorization; +using CodeBeam.UltimateAuth.Authorization.Contracts; using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.InMemory; -namespace CodeBeam.UltimateAuth.Authorization.InMemory; +namespace CodeBeam.UltimateAuth.Sample.Seed; -internal sealed class InMemoryAuthorizationSeedContributor : ISeedContributor +internal sealed class AuthorizationSeedContributor : ISeedContributor { public int Order => 20; private readonly IRoleStoreFactory _roleStoreFactory; private readonly IUserRoleStoreFactory _userRoleStoreFactory; - private readonly IInMemoryUserIdProvider _ids; + private readonly IUserIdProvider _ids; private readonly IClock _clock; - public InMemoryAuthorizationSeedContributor( + public AuthorizationSeedContributor( IRoleStoreFactory roleStoreFactory, IUserRoleStoreFactory userRoleStoreFactory, - IInMemoryUserIdProvider ids, + IUserIdProvider ids, IClock clock) { _roleStoreFactory = roleStoreFactory; diff --git a/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj b/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj new file mode 100644 index 00000000..96d9adaa --- /dev/null +++ b/samples/CodeBeam.UltimateAuth.Sample.Seed/CodeBeam.UltimateAuth.Sample.Seed.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs b/samples/CodeBeam.UltimateAuth.Sample.Seed/CredentialSeedContributor.cs similarity index 78% rename from src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs rename to samples/CodeBeam.UltimateAuth.Sample.Seed/CredentialSeedContributor.cs index 84b01dbc..b49f8753 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/InMemoryCredentialSeedContributor.cs +++ b/samples/CodeBeam.UltimateAuth.Sample.Seed/CredentialSeedContributor.cs @@ -4,22 +4,21 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Credentials.Contracts; using CodeBeam.UltimateAuth.Credentials.Reference; -using CodeBeam.UltimateAuth.InMemory; -namespace CodeBeam.UltimateAuth.Credentials.InMemory; +namespace CodeBeam.UltimateAuth.Sample.Seed; -internal sealed class InMemoryCredentialSeedContributor : ISeedContributor +internal sealed class CredentialSeedContributor : ISeedContributor { private static readonly Guid _adminPasswordId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); private static readonly Guid _userPasswordId = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"); public int Order => 10; private readonly IPasswordCredentialStoreFactory _credentialFactory; - private readonly IInMemoryUserIdProvider _ids; + private readonly IUserIdProvider _ids; private readonly IUAuthPasswordHasher _hasher; private readonly IClock _clock; - public InMemoryCredentialSeedContributor(IPasswordCredentialStoreFactory credentialFactory, IInMemoryUserIdProvider ids, IUAuthPasswordHasher hasher, IClock clock) + public CredentialSeedContributor(IPasswordCredentialStoreFactory credentialFactory, IUserIdProvider ids, IUAuthPasswordHasher hasher, IClock clock) { _credentialFactory = credentialFactory; _ids = ids; @@ -38,6 +37,12 @@ private async Task SeedCredentialAsync(UserKey userKey, Guid credentialId, strin try { var credentialStore = _credentialFactory.Create(tenant); + + var existing = await credentialStore.GetByUserAsync(userKey, ct); + + if (existing.Any(x => x.Id == credentialId)) + return; + await credentialStore.AddAsync( PasswordCredential.Create( credentialId, diff --git a/samples/CodeBeam.UltimateAuth.Sample.Seed/Extensions/ServiceCollectionExtensions.cs b/samples/CodeBeam.UltimateAuth.Sample.Seed/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..25a28910 --- /dev/null +++ b/samples/CodeBeam.UltimateAuth.Sample.Seed/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,32 @@ +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace CodeBeam.UltimateAuth.Sample.Seed.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddUltimateAuthSampleSeed(this IServiceCollection services) + { + services.TryAddSingleton(); + services.AddSingleton, UserIdProvider>(); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + return services; + } + + public static IServiceCollection AddScopedUltimateAuthSampleSeed(this IServiceCollection services) + { + services.TryAddScoped(); + services.AddSingleton, UserIdProvider>(); + services.TryAddEnumerable(ServiceDescriptor.Scoped()); + services.TryAddEnumerable(ServiceDescriptor.Scoped()); + services.TryAddEnumerable(ServiceDescriptor.Scoped()); + + return services; + } +} diff --git a/samples/CodeBeam.UltimateAuth.Sample.Seed/IUserIdProvider.cs b/samples/CodeBeam.UltimateAuth.Sample.Seed/IUserIdProvider.cs new file mode 100644 index 00000000..6d4e04be --- /dev/null +++ b/samples/CodeBeam.UltimateAuth.Sample.Seed/IUserIdProvider.cs @@ -0,0 +1,7 @@ +namespace CodeBeam.UltimateAuth.Sample.Seed; + +public interface IUserIdProvider +{ + TUserId GetAdminUserId(); + TUserId GetUserUserId(); +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserIdProvider.cs b/samples/CodeBeam.UltimateAuth.Sample.Seed/UserIdProvider.cs similarity index 69% rename from src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserIdProvider.cs rename to samples/CodeBeam.UltimateAuth.Sample.Seed/UserIdProvider.cs index 0d6f4ec8..e2a167bb 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserIdProvider.cs +++ b/samples/CodeBeam.UltimateAuth.Sample.Seed/UserIdProvider.cs @@ -1,9 +1,8 @@ using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.InMemory; -namespace CodeBeam.UltimateAuth.Users.InMemory; +namespace CodeBeam.UltimateAuth.Sample.Seed; -public sealed class InMemoryUserIdProvider : IInMemoryUserIdProvider +public sealed class UserIdProvider : IUserIdProvider { private static readonly UserKey Admin = UserKey.FromGuid(Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")); private static readonly UserKey User = UserKey.FromGuid(Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")); diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs b/samples/CodeBeam.UltimateAuth.Sample.Seed/UserSeedContributor.cs similarity index 88% rename from src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs rename to samples/CodeBeam.UltimateAuth.Sample.Seed/UserSeedContributor.cs index 659f30d2..5d285880 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Infrastructure/InMemoryUserSeedContributor.cs +++ b/samples/CodeBeam.UltimateAuth.Sample.Seed/UserSeedContributor.cs @@ -1,29 +1,28 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.InMemory; using CodeBeam.UltimateAuth.Server.Infrastructure; using CodeBeam.UltimateAuth.Users.Contracts; using CodeBeam.UltimateAuth.Users.Reference; -namespace CodeBeam.UltimateAuth.Users.InMemory; +namespace CodeBeam.UltimateAuth.Sample.Seed; -internal sealed class InMemoryUserSeedContributor : ISeedContributor +public sealed class UserSeedContributor : ISeedContributor { public int Order => 0; private readonly IUserLifecycleStoreFactory _lifecycleFactory; private readonly IUserIdentifierStoreFactory _identifierFactory; private readonly IUserProfileStoreFactory _profileFactory; - private readonly IInMemoryUserIdProvider _ids; + private readonly IUserIdProvider _ids; private readonly IIdentifierNormalizer _identifierNormalizer; private readonly IClock _clock; - public InMemoryUserSeedContributor( + public UserSeedContributor( IUserLifecycleStoreFactory lifecycleFactory, IUserProfileStoreFactory profileFactory, IUserIdentifierStoreFactory identifierFactory, - IInMemoryUserIdProvider ids, + IUserIdProvider ids, IIdentifierNormalizer identifierNormalizer, IClock clock) { @@ -68,10 +67,7 @@ await profileStore.AddAsync( ct); } - async Task EnsureIdentifier( - UserIdentifierType type, - string value, - bool isPrimary) + async Task EnsureIdentifier(UserIdentifierType type, string value, bool isPrimary) { var normalized = _identifierNormalizer .Normalize(type, value).Normalized; diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj index fbf939d6..840af929 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.csproj @@ -21,6 +21,7 @@ + diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs index 47a813c8..9108573e 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub/Program.cs @@ -3,6 +3,7 @@ using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Infrastructure; using CodeBeam.UltimateAuth.InMemory; +using CodeBeam.UltimateAuth.Sample.Seed.Extensions; using CodeBeam.UltimateAuth.Sample.UAuthHub.Components; using CodeBeam.UltimateAuth.Sample.UAuthHub.Infrastructure; using CodeBeam.UltimateAuth.Server.Extensions; @@ -41,6 +42,8 @@ .AddUltimateAuthInMemory() .AddUAuthHub(o => o.AllowedClientOrigins.Add("https://localhost:6130")); // Client sample's URL +builder.Services.AddUltimateAuthSampleSeed(); + builder.Services.AddUltimateAuthClientBlazor(o => { //o.Refresh.Interval = TimeSpan.FromSeconds(5); diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Brand/UAuthLogo.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Brand/UAuthLogo.razor new file mode 100644 index 00000000..2806b7d3 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Brand/UAuthLogo.razor @@ -0,0 +1,19 @@ +@namespace CodeBeam.UltimateAuth.Sample +@inherits ComponentBase + + + + @if (Variant == UAuthLogoVariant.Brand) + { + + + + } + else + { + + + + } + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Brand/UAuthLogo.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Brand/UAuthLogo.razor.cs new file mode 100644 index 00000000..030d9b66 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Brand/UAuthLogo.razor.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + +namespace CodeBeam.UltimateAuth.Sample; + +public partial class UAuthLogo : ComponentBase +{ + [Parameter] public UAuthLogoVariant Variant { get; set; } = UAuthLogoVariant.Brand; + + [Parameter] public int Size { get; set; } = 32; + + [Parameter] public string? ShieldColor { get; set; } = "#00072d"; + [Parameter] public string? KeyColor { get; set; } = "#f6f5ae"; + + [Parameter] public string? Class { get; set; } + [Parameter] public string? Style { get; set; } + + private string BuildStyle() + { + if (Variant == UAuthLogoVariant.Mono) + return $"color: {KeyColor}; {Style}"; + + return Style ?? ""; + } + + protected string KeyPath => @" +M120.43,39.44H79.57A11.67,11.67,0,0,0,67.9,51.11V77.37 +A11.67,11.67,0,0,0,79.57,89H90.51l3.89,3.9v5.32l-3.8,3.81v81.41H99 +v-5.33h13.69V169H108.1v-3.8H99C99,150.76,111.9,153,111.9,153 +V99.79h-8V93.32L108.19,89h12.24 +A11.67,11.67,0,0,0,132.1,77.37V51.11 +A11.67,11.67,0,0,0,120.43,39.44Z + +M79.57,48.19h5.84a2.92,2.92 0 0 1 2.92,2.92 +v5.84a2.92,2.92 0 0 1 -2.92,2.92 +h-5.84a2.91,2.91 0 0 1 -2.91,-2.92 +v-5.84a2.91,2.91 0 0 1 2.91,-2.92Z + +M79.57,68.62h5.84a2.92,2.92 0 0 1 2.92,2.92 +v5.83a2.92,2.92 0 0 1 -2.92,2.92 +h-5.84a2.91,2.91 0 0 1 -2.91,-2.92 +v-5.83a2.91,2.91 0 0 1 2.91,-2.92Z + +M114.59,48.19h5.84a2.92,2.92 0 0 1 2.91,2.92 +v5.84a2.91,2.91 0 0 1 -2.91,2.91 +h-5.84a2.92,2.92 0 0 1 -2.92,-2.91 +v-5.84a2.92,2.92 0 0 1 2.92,-2.92Z + +M114.59,68.62h5.84a2.92,2.92 0 0 1 2.91,2.92 +v5.83a2.91,2.91 0 0 1 -2.91,2.92 +h-5.84a2.92,2.92 0 0 1 -2.92,-2.92 +v-5.83a2.92,2.92 0 0 1 2.92,-2.92Z +"; +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Brand/UAuthLogoVariant.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Brand/UAuthLogoVariant.cs new file mode 100644 index 00000000..fe3be220 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Brand/UAuthLogoVariant.cs @@ -0,0 +1,7 @@ +namespace CodeBeam.UltimateAuth.Sample; + +public enum UAuthLogoVariant +{ + Brand, + Mono +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.csproj b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.csproj new file mode 100644 index 00000000..f49ac077 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.csproj @@ -0,0 +1,33 @@ + + + + net10.0 + enable + enable + false + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Common/UAuthDialog.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Common/UAuthDialog.cs new file mode 100644 index 00000000..2183c80d --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Common/UAuthDialog.cs @@ -0,0 +1,29 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Domain; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Common; + +public static class UAuthDialog +{ + public static DialogParameters GetDialogParameters(UAuthState state, UserKey? userKey = null) + { + DialogParameters parameters = new DialogParameters(); + parameters.Add("AuthState", state); + if (userKey != null ) + { + parameters.Add("UserKey", userKey); + } + return parameters; + } + + public static DialogOptions GetDialogOptions(MaxWidth maxWidth = MaxWidth.Medium) + { + return new DialogOptions + { + MaxWidth = maxWidth, + FullWidth = true, + CloseButton = true + }; + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/App.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/App.razor new file mode 100644 index 00000000..6acc099e --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/App.razor @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Custom/UAuthPageComponent.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Custom/UAuthPageComponent.razor new file mode 100644 index 00000000..5af543e4 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Custom/UAuthPageComponent.razor @@ -0,0 +1,10 @@ + + + @ChildContent + + + +@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/AccountStatusDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/AccountStatusDialog.razor new file mode 100644 index 00000000..0c91e45c --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/AccountStatusDialog.razor @@ -0,0 +1,23 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService + + + + Identifier Management + User: @AuthState?.Identity?.DisplayName + + + + + Suspend Account + + + + Delete Account + + + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/AccountStatusDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/AccountStatusDialog.razor.cs new file mode 100644 index 00000000..edeedaa0 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/AccountStatusDialog.razor.cs @@ -0,0 +1,77 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs; + +public partial class AccountStatusDialog +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + private async Task SuspendAccountAsync() + { + var info = await DialogService.ShowMessageBoxAsync( + title: "Are You Sure", + markupMessage: (MarkupString) + """ + You are going to suspend your account.

+ You can still active your account later. + """, + yesText: "Suspend", noText: "Cancel", + options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" }); + + if (info != true) + { + Snackbar.Add("Suspend process cancelled.", Severity.Info); + return; + } + + ChangeUserStatusSelfRequest request = new() { NewStatus = SelfUserStatus.SelfSuspended }; + var result = await UAuthClient.Users.ChangeStatusSelfAsync(request); + if (result.IsSuccess) + { + Snackbar.Add("Your account suspended successfully.", Severity.Success); + MudDialog.Close(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Delete failed.", Severity.Error); + } + } + + private async Task DeleteAccountAsync() + { + var info = await DialogService.ShowMessageBoxAsync( + title: "Are You Sure", + markupMessage: (MarkupString) + """ + You are going to delete your account.

+ This action can't be undone.

+ (Actually it is, admin can handle soft deleted accounts.) + """, + yesText: "Delete", noText: "Cancel", + options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" }); + + if (info != true) + { + Snackbar.Add("Deletion cancelled.", Severity.Info); + return; + } + + var result = await UAuthClient.Users.DeleteMeAsync(); + if (result.IsSuccess) + { + Snackbar.Add("Your account deleted successfully.", Severity.Success); + MudDialog.Close(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Delete failed.", Severity.Error); + } + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CreateUserDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CreateUserDialog.razor new file mode 100644 index 00000000..9a514935 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CreateUserDialog.razor @@ -0,0 +1,27 @@ +@using CodeBeam.UltimateAuth.Credentials.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar + + + + Create User + + + + + + + + + + + + + + + + Cancel + Create + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CreateUserDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CreateUserDialog.razor.cs new file mode 100644 index 00000000..778ad577 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CreateUserDialog.razor.cs @@ -0,0 +1,55 @@ +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs; + +public partial class CreateUserDialog +{ + private MudForm _form = null!; + private string? _username; + private string? _email; + private string? _password; + private string? _passwordCheck; + private string? _displayName; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + private async Task CreateUserAsync() + { + await _form.Validate(); + + if (!_form.IsValid) + return; + + if (_password != _passwordCheck) + { + Snackbar.Add("Passwords don't match.", Severity.Error); + return; + } + + var request = new CreateUserRequest + { + UserName = _username, + Email = _email, + DisplayName = _displayName, + Password = _password + }; + + var result = await UAuthClient.Users.CreateAdminAsync(request); + + if (!result.IsSuccess) + { + Snackbar.Add(result.ErrorText ?? "User creation failed.", Severity.Error); + return; + } + + Snackbar.Add("User created successfully", Severity.Success); + MudDialog.Close(DialogResult.Ok(true)); + } + + private string PasswordMatch(string? arg) => _password != arg ? "Passwords don't match." : string.Empty; + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CredentialDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CredentialDialog.razor new file mode 100644 index 00000000..660b7c3a --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CredentialDialog.razor @@ -0,0 +1,51 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Credentials.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject IUAuthStateManager StateManager +@inject NavigationManager Nav + + + + Credential Management + User: @AuthState?.Identity?.DisplayName + + + + + @if (UserKey == null) + { + + + + } + else + { + + + Administrators can directly assign passwords to users. + However, using the credential reset flow is generally recommended for better security and auditability. + + + } + + + + + + + + + + + @(UserKey is null ? "Change Password" : "Set Password") + + + + + + Cancel + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CredentialDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CredentialDialog.razor.cs new file mode 100644 index 00000000..f9829141 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/CredentialDialog.razor.cs @@ -0,0 +1,92 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Credentials.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs; + +public partial class CredentialDialog +{ + private MudForm _form = null!; + private string? _oldPassword; + private string? _newPassword; + private string? _newPasswordCheck; + private bool _passwordMode1 = false; + private bool _passwordMode2 = false; + private bool _passwordMode3 = true; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + [Parameter] + public UserKey? UserKey { get; set; } + + private async Task ChangePasswordAsync() + { + if (_form is null) + return; + + await _form.Validate(); + if (!_form.IsValid) + { + Snackbar.Add("Form is not valid.", Severity.Error); + return; + } + + if (_newPassword != _newPasswordCheck) + { + Snackbar.Add("New password and check do not match", Severity.Error); + return; + } + + ChangeCredentialRequest request; + + if (UserKey is null) + { + request = new ChangeCredentialRequest + { + CurrentSecret = _oldPassword!, + NewSecret = _newPassword! + }; + } + else + { + request = new ChangeCredentialRequest + { + NewSecret = _newPassword! + }; + } + + UAuthResult result; + if (UserKey is null) + { + result = await UAuthClient.Credentials.ChangeMyAsync(request); + } + else + { + result = await UAuthClient.Credentials.ChangeCredentialAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Password changed successfully", Severity.Success); + _oldPassword = null; + _newPassword = null; + _newPasswordCheck = null; + MudDialog.Close(DialogResult.Ok(true)); + } + else + { + Snackbar.Add(result.ErrorText ?? "An error occurred while changing password", Severity.Error); + } + } + + private string PasswordMatch(string arg) => _newPassword != arg ? "Passwords don't match" : string.Empty; + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/IdentifierDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/IdentifierDialog.razor new file mode 100644 index 00000000..0d631533 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/IdentifierDialog.razor @@ -0,0 +1,106 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService + + + + Identifier Management + + @if (UserKey is null) + { + User: @AuthState?.Identity?.DisplayName + } + else + { + UserKey: @UserKey.Value + } + + + + + + + Identifiers + + + + + + + + + + + + + + + + + + + + + + + + + @if (context.Item.IsPrimary) + { + + + + } + else + { + + + + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add + + + + + + + + Cancel + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/IdentifierDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/IdentifierDialog.razor.cs new file mode 100644 index 00000000..c07885b3 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/IdentifierDialog.razor.cs @@ -0,0 +1,309 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs; + +public partial class IdentifierDialog +{ + private MudDataGrid? _grid; + private UserIdentifierType _newIdentifierType; + private string? _newIdentifierValue; + private bool _newIdentifierPrimary; + private bool _loading = false; + private bool _reloadQueued; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + [Parameter] + public UserKey? UserKey { get; set; } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (firstRender) + { + var result = await UAuthClient.Identifiers.GetMyIdentifiersAsync(); + if (result != null && result.IsSuccess && result.Value != null) + { + await ReloadAsync(); + StateHasChanged(); + } + } + } + + private async Task> LoadServerData(GridState state, CancellationToken ct) + { + var sort = state.SortDefinitions?.FirstOrDefault(); + + var req = new PageRequest + { + PageNumber = state.Page + 1, + PageSize = state.PageSize, + SortBy = sort?.SortBy, + Descending = sort?.Descending ?? false + }; + + UAuthResult> res; + + if (UserKey is null) + { + res = await UAuthClient.Identifiers.GetMyIdentifiersAsync(req); + } + else + { + res = await UAuthClient.Identifiers.GetUserIdentifiersAsync(UserKey.Value, req); + } + + if (!res.IsSuccess || res.Value is null) + { + Snackbar.Add(res.Problem?.Title ?? "Failed", Severity.Error); + + return new GridData + { + Items = Array.Empty(), + TotalItems = 0 + }; + } + + return new GridData + { + Items = res.Value.Items, + TotalItems = res.Value.TotalCount + }; + } + + private async Task ReloadAsync() + { + if (_loading) + { + _reloadQueued = true; + return; + } + + if (_grid is null) + return; + + _loading = true; + await InvokeAsync(StateHasChanged); + await Task.Delay(300); + + try + { + await _grid.ReloadServerData(); + } + finally + { + _loading = false; + + if (_reloadQueued) + { + _reloadQueued = false; + await ReloadAsync(); + } + + await InvokeAsync(StateHasChanged); + } + } + + private async Task CommittedItemChanges(UserIdentifierInfo item) + { + UpdateUserIdentifierRequest updateRequest = new() + { + Id = item.Id, + NewValue = item.Value + }; + + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.UpdateSelfAsync(updateRequest); + } + else + { + result = await UAuthClient.Identifiers.UpdateAdminAsync(UserKey.Value, updateRequest); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier updated successfully", Severity.Success); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to update identifier", Severity.Error); + } + + await ReloadAsync(); + return DataGridEditFormAction.Close; + } + + private async Task AddNewIdentifier() + { + if (string.IsNullOrEmpty(_newIdentifierValue)) + { + Snackbar.Add("Value cannot be empty", Severity.Warning); + return; + } + + AddUserIdentifierRequest request = new() + { + Type = _newIdentifierType, + Value = _newIdentifierValue, + IsPrimary = _newIdentifierPrimary + }; + + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.AddSelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.AddAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier added successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to add identifier", Severity.Error); + } + } + + private async Task VerifyAsync(Guid id) + { + var demoInfo = await DialogService.ShowMessageBoxAsync( + title: "Demo verification", + markupMessage: (MarkupString) + """ + This is a demo action.

+ In a real app, you should verify identifiers via Email, SMS, or an Authenticator flow. + This will only mark the identifier as verified in UltimateAuth. + """, + yesText: "Verify", noText: "Cancel", + options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" }); + + if (demoInfo != true) + { + Snackbar.Add("Verification cancelled", Severity.Info); + return; + } + + VerifyUserIdentifierRequest request = new() { IdentifierId = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.VerifySelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.VerifyAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier verified successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to verify primary identifier", Severity.Error); + } + } + + private async Task SetPrimaryAsync(Guid id) + { + SetPrimaryUserIdentifierRequest request = new() { IdentifierId = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.SetPrimarySelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.SetPrimaryAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Primary identifier set successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to set primary identifier", Severity.Error); + } + } + + private async Task UnsetPrimaryAsync(Guid id) + { + UnsetPrimaryUserIdentifierRequest request = new() { IdentifierId = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.UnsetPrimarySelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.UnsetPrimaryAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Primary identifier unset successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to unset primary identifier", Severity.Error); + } + } + + private async Task DeleteIdentifier(Guid id) + { + DeleteUserIdentifierRequest request = new() { IdentifierId = id }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Identifiers.DeleteSelfAsync(request); + } + else + { + result = await UAuthClient.Identifiers.DeleteAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Identifier deleted successfully", Severity.Success); + await ReloadAsync(); + StateHasChanged(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to delete identifier", Severity.Error); + } + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/PermissionDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/PermissionDialog.razor new file mode 100644 index 00000000..8e0df863 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/PermissionDialog.razor @@ -0,0 +1,46 @@ +@using CodeBeam.UltimateAuth.Authorization.Contracts +@using CodeBeam.UltimateAuth.Core.Defaults +@using System.Reflection + +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar + + + + Role Permissions + @Role.Name + + + + @* For Debug *@ + @* Current Permissions: @string.Join(", ", Role.Permissions) *@ + + @foreach (var group in _groups) + { + + + + + @group.Name (@group.Items.Count(x => x.Selected)/@group.Items.Count) + + + + + @foreach (var perm in group.Items) + { + + + + } + + + + } + + + + + Cancel + Save + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/PermissionDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/PermissionDialog.razor.cs new file mode 100644 index 00000000..99c3b75b --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/PermissionDialog.razor.cs @@ -0,0 +1,119 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs; + +public partial class PermissionDialog +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public RoleInfo Role { get; set; } = default!; + + private List _groups = new(); + + protected override void OnInitialized() + { + var catalog = UAuthPermissionCatalog.GetAdminPermissions(); + var expanded = PermissionExpander.Expand(Role.Permissions, catalog); + var selected = expanded.Select(x => x.Value).ToHashSet(); + + _groups = catalog + .GroupBy(p => p.Split('.')[0]) + .Select(g => new PermissionGroup + { + Name = g.Key, + Items = g.Select(p => new PermissionItem + { + Value = p, + Selected = selected.Contains(p) + }).ToList() + }) + .OrderBy(x => x.Name) + .ToList(); + } + + private void ToggleGroup(PermissionGroup group, bool value) + { + foreach (var item in group.Items) + item.Selected = value; + } + + private void TogglePermission(PermissionItem item, bool value) + { + item.Selected = value; + } + + private bool? GetGroupState(PermissionGroup group) + { + var selected = group.Items.Count(x => x.Selected); + + if (selected == 0) + return false; + + if (selected == group.Items.Count) + return true; + + return null; + } + + private async Task Save() + { + var permissions = _groups.SelectMany(g => g.Items).Where(x => x.Selected).Select(x => Permission.From(x.Value)).ToList(); + + var req = new SetPermissionsRequest + { + Permissions = permissions + }; + + var result = await UAuthClient.Authorization.SetPermissionsAsync(Role.Id, req); + + if (!result.IsSuccess) + { + Snackbar.Add(result.ErrorText ?? "Failed to update permissions", Severity.Error); + return; + } + + var result2 = await UAuthClient.Authorization.QueryRolesAsync(new RoleQuery() { Search = Role.Name }); + if (result2.Value?.Items is not null) + { + Role = result2.Value.Items.First(); + } + + Snackbar.Add("Permissions updated", Severity.Success); + RefreshUI(); + } + + private void RefreshUI() + { + var catalog = UAuthPermissionCatalog.GetAdminPermissions(); + var expanded = PermissionExpander.Expand(Role.Permissions, catalog); + var selected = expanded.Select(x => x.Value).ToHashSet(); + + foreach (var group in _groups) + { + foreach (var item in group.Items) + { + item.Selected = selected.Contains(item.Value); + } + } + + StateHasChanged(); + } + + private void Cancel() => MudDialog.Cancel(); + + private class PermissionGroup + { + public string Name { get; set; } = ""; + public List Items { get; set; } = new(); + } + + private class PermissionItem + { + public string Value { get; set; } = ""; + public bool Selected { get; set; } + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ProfileDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ProfileDialog.razor new file mode 100644 index 00000000..d09fcfa0 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ProfileDialog.razor @@ -0,0 +1,94 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService + + + + Identifier Management + + @if (UserKey is null) + { + User: @AuthState?.Identity?.DisplayName + } + else + { + UserKey: @UserKey.Value + } + + + + + + + + + Name + + + + + + + + + + + + + + + + + + + Personal + + + + + + + + + + + + + + + + + + + Localization + + + + + + + + + + + @foreach (var tz in TimeZoneInfo.GetSystemTimeZones()) + { + @tz.Id - @tz.DisplayName + } + + + + + + + + + + + + Cancel + Save + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ProfileDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ProfileDialog.razor.cs new file mode 100644 index 00000000..868c9c05 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ProfileDialog.razor.cs @@ -0,0 +1,114 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs; + +public partial class ProfileDialog +{ + private MudForm? _form; + private string? _firstName; + private string? _lastName; + private string? _displayName; + private DateTime? _birthDate; + private string? _gender; + private string? _bio; + private string? _language; + private string? _timeZone; + private string? _culture; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + [Parameter] + public UserKey? UserKey { get; set; } + + protected override async Task OnInitializedAsync() + { + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Users.GetMeAsync(); + } + else + { + result = await UAuthClient.Users.GetProfileAsync(UserKey.Value); + } + + if (result.IsSuccess && result.Value is not null) + { + var p = result.Value; + + _firstName = p.FirstName; + _lastName = p.LastName; + _displayName = p.DisplayName; + + _gender = p.Gender; + _birthDate = p.BirthDate?.ToDateTime(TimeOnly.MinValue); + _bio = p.Bio; + + _language = p.Language; + _timeZone = p.TimeZone; + _culture = p.Culture; + } + } + + private async Task SaveAsync() + { + if (AuthState is null || AuthState.Identity is null) + { + Snackbar.Add("No AuthState found.", Severity.Error); + return; + } + + if (_form is not null) + { + await _form.Validate(); + if (!_form.IsValid) + return; + } + + var request = new UpdateProfileRequest + { + FirstName = _firstName, + LastName = _lastName, + DisplayName = _displayName, + BirthDate = _birthDate.HasValue ? DateOnly.FromDateTime(_birthDate.Value) : null, + Gender = _gender, + Bio = _bio, + Language = _language, + TimeZone = _timeZone, + Culture = _culture + }; + + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Users.UpdateMeAsync(request); + } + else + { + result = await UAuthClient.Users.UpdateProfileAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Profile updated", Severity.Success); + MudDialog.Close(DialogResult.Ok(true)); + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed to update profile", Severity.Error); + } + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ResetDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ResetDialog.razor new file mode 100644 index 00000000..06a515aa --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ResetDialog.razor @@ -0,0 +1,38 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Credentials.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject IUAuthStateManager StateManager +@inject NavigationManager Nav + + + + Reset Credential + + + + + + This is a demonstration of how to implement a credential reset flow. + In a production application, you should use reset token or code in email, SMS etc. verification steps. + + + Reset request always returns ok even with not found users due to security reasons. + + + Request Reset + @if (_resetRequested) + { + Your reset code is: (Copy it before next step) + @_resetCode + Use Reset Code + } + + + + + Close + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ResetDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ResetDialog.razor.cs new file mode 100644 index 00000000..55f5195c --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/ResetDialog.razor.cs @@ -0,0 +1,42 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Credentials.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs; + +public partial class ResetDialog +{ + private bool _resetRequested = false; + private string? _resetCode; + private string? _identifier; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + private async Task RequestResetAsync() + { + var request = new BeginCredentialResetRequest + { + CredentialType = CredentialType.Password, + ResetCodeType = ResetCodeType.Code, + Identifier = _identifier ?? string.Empty + }; + + var result = await UAuthClient.Credentials.BeginResetMyAsync(request); + if (!result.IsSuccess || result.Value is null) + { + Snackbar.Add(result.ErrorText ?? "Failed to request credential reset.", Severity.Error); + return; + } + + _resetCode = result.Value.Token; + _resetRequested = true; + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/RoleDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/RoleDialog.razor new file mode 100644 index 00000000..b78db16f --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/RoleDialog.razor @@ -0,0 +1,81 @@ +@using CodeBeam.UltimateAuth.Authorization.Contracts +@using CodeBeam.UltimateAuth.Core.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService + + + + + Role Management + Manage system roles + + + + + + + + Roles + + + + + + + + + + + @GetPermissionCount(context.Item) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + + + + + + + + Close + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/RoleDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/RoleDialog.razor.cs new file mode 100644 index 00000000..349ef670 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/RoleDialog.razor.cs @@ -0,0 +1,163 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Client; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs; + +public partial class RoleDialog +{ + private MudDataGrid? _grid; + private bool _loading; + private string? _newRoleName; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + private async Task> LoadServerData(GridState state, CancellationToken ct) + { + var sort = state.SortDefinitions?.FirstOrDefault(); + + var req = new RoleQuery + { + PageNumber = state.Page + 1, + PageSize = state.PageSize, + SortBy = sort?.SortBy, + Descending = sort?.Descending ?? false + }; + + var res = await UAuthClient.Authorization.QueryRolesAsync(req); + + if (!res.IsSuccess || res.Value == null) + { + Snackbar.Add(res.ErrorText ?? "Failed", Severity.Error); + + return new GridData + { + Items = Array.Empty(), + TotalItems = 0 + }; + } + + return new GridData + { + Items = res.Value.Items, + TotalItems = res.Value.TotalCount + }; + } + + private async Task CommittedItemChanges(RoleInfo role) + { + var req = new RenameRoleRequest + { + Name = role.Name + }; + + var result = await UAuthClient.Authorization.RenameRoleAsync(role.Id, req); + + if (result.IsSuccess) + { + Snackbar.Add("Role renamed", Severity.Success); + } + else + { + Snackbar.Add(result.ErrorText ?? "Rename failed", Severity.Error); + } + + await ReloadAsync(); + return DataGridEditFormAction.Close; + } + + private async Task CreateRole() + { + if (string.IsNullOrWhiteSpace(_newRoleName)) + { + Snackbar.Add("Role name required.", Severity.Warning); + return; + } + + var req = new CreateRoleRequest + { + Name = _newRoleName + }; + + var res = await UAuthClient.Authorization.CreateRoleAsync(req); + + if (res.IsSuccess) + { + Snackbar.Add("Role created.", Severity.Success); + await ReloadAsync(); + } + else + { + Snackbar.Add(res.ErrorText ?? "Creation failed.", Severity.Error); + } + } + + private async Task DeleteRole(RoleId roleId) + { + var confirm = await DialogService.ShowMessageBoxAsync( + "Delete role", + "Are you sure?", + yesText: "Delete", + cancelText: "Cancel", + options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" }); + + if (confirm != true) + return; + + var req = new DeleteRoleRequest(); + var result = await UAuthClient.Authorization.DeleteRoleAsync(roleId, req); + + if (result.IsSuccess) + { + Snackbar.Add($"Role deleted, assignments removed from {result.Value?.RemovedAssignments.ToString() ?? "unknown"} users.", Severity.Success); + await ReloadAsync(); + } + else + { + Snackbar.Add(result.ErrorText ?? "Deletion failed.", Severity.Error); + } + } + + private async Task EditPermissions(RoleInfo role) + { + var dialog = await DialogService.ShowAsync( + "Edit Permissions", + new DialogParameters + { + { nameof(PermissionDialog.Role), role } + }, + new DialogOptions + { + CloseButton = true, + MaxWidth = MaxWidth.Large, + FullWidth = true + }); + + var result = await dialog.Result; + await ReloadAsync(); + } + + private async Task ReloadAsync() + { + _loading = true; + await Task.Delay(300); + if (_grid is null) + return; + + await _grid.ReloadServerData(); + _loading = false; + } + + private int GetPermissionCount(RoleInfo role) + { + var expanded = PermissionExpander.Expand(role.Permissions, UAuthPermissionCatalog.GetAdminPermissions()); + return expanded.Count; + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/SessionDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/SessionDialog.razor new file mode 100644 index 00000000..8ecf2a15 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/SessionDialog.razor @@ -0,0 +1,217 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject IUAuthStateManager StateManager +@inject NavigationManager Nav + + + + Session Management + + @if (UserKey is null) + { + User: @AuthState?.Identity?.DisplayName + } + else + { + UserKey: @UserKey.Value + } + + + + @if (_chainDetail is not null) + { + + + + Device Details + + + + @if (!_chainDetail.IsRevoked) + { + + Revoke Device + + } + + + + + + + Device Type + @_chainDetail.DeviceType + + + + Platform + @_chainDetail.Platform + + + + Operating System + @_chainDetail.OperatingSystem + + + + Browser + @_chainDetail.Browser + + + + Created + @_chainDetail.CreatedAt.ToLocalTime() + + + + Last Seen + @_chainDetail.LastSeenAt.ToLocalTime() + + + + State + + @_chainDetail.State + + + + + Active Session + @_chainDetail.ActiveSessionId + + + + Rotation Count + @_chainDetail.RotationCount + + + + Touch Count + @_chainDetail.TouchCount + + + + + + Session History + + + + Session Id + Created + Expires + Status + + + + @context.SessionId + @context.CreatedAt.ToLocalTime() + @context.ExpiresAt.ToLocalTime() + + @if (context.IsRevoked) + { + Revoked + } + else + { + Active + } + + + + + } + else + { + + Logout All Devices + @if (UserKey == null) + { + Logout Other Devices + } + Revoke All Devices + @if (UserKey == null) + { + Revoke Other Devices + } + + + + Sessions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Id + @context.Item.ChainId + + + + Created At + @context.Item.CreatedAt + + + + Touch Count + @context.Item.TouchCount + + + + Rotation Count + @context.Item.RotationCount + + + + + + + + + } + + + Cancel + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/SessionDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/SessionDialog.razor.cs new file mode 100644 index 00000000..5ec8d396 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/SessionDialog.razor.cs @@ -0,0 +1,284 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs; + +public partial class SessionDialog +{ + private MudDataGrid? _grid; + private bool _loading = false; + private bool _reloadQueued; + private SessionChainDetail? _chainDetail; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + [Parameter] + public UserKey? UserKey { get; set; } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (firstRender) + { + var result = await UAuthClient.Sessions.GetMyChainsAsync(); + if (result != null && result.IsSuccess && result.Value != null) + { + await ReloadAsync(); + StateHasChanged(); + } + } + } + + private async Task> LoadServerData(GridState state, CancellationToken ct) + { + var sort = state.SortDefinitions?.FirstOrDefault(); + + var req = new PageRequest + { + PageNumber = state.Page + 1, + PageSize = state.PageSize, + SortBy = sort?.SortBy, + Descending = sort?.Descending ?? false + }; + + UAuthResult> res; + + if (UserKey is null) + { + res = await UAuthClient.Sessions.GetMyChainsAsync(req); + } + else + { + res = await UAuthClient.Sessions.GetUserChainsAsync(UserKey.Value, req); + } + + if (!res.IsSuccess || res.Value is null) + { + Snackbar.Add(res.Problem?.Title ?? "Failed", Severity.Error); + + return new GridData + { + Items = Array.Empty(), + TotalItems = 0 + }; + } + + return new GridData + { + Items = res.Value.Items, + TotalItems = res.Value.TotalCount + }; + } + + private async Task ReloadAsync() + { + if (_loading) + { + _reloadQueued = true; + return; + } + + if (_grid is null) + return; + + _loading = true; + await InvokeAsync(StateHasChanged); + await Task.Delay(300); + + try + { + await _grid.ReloadServerData(); + } + finally + { + _loading = false; + + if (_reloadQueued) + { + _reloadQueued = false; + await ReloadAsync(); + } + + await InvokeAsync(StateHasChanged); + } + } + + private async Task LogoutAllAsync() + { + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Flows.LogoutAllDevicesSelfAsync(); + } + else + { + result = await UAuthClient.Flows.LogoutAllDevicesAdminAsync(UserKey.Value); + } + + if (result.IsSuccess) + { + Snackbar.Add("Logged out of all devices.", Severity.Success); + if (UserKey is null) + Nav.NavigateTo("/login"); + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed to logout.", Severity.Error); + } + } + + private async Task LogoutOthersAsync() + { + var result = await UAuthClient.Flows.LogoutOtherDevicesSelfAsync(); + + if (result.IsSuccess) + { + Snackbar.Add("Logged out of other devices.", Severity.Success); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to logout", Severity.Error); + } + } + + private async Task LogoutDeviceAsync(SessionChainId chainId) + { + LogoutDeviceRequest request = new() { ChainId = chainId }; + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Flows.LogoutDeviceSelfAsync(request); + } + else + { + result = await UAuthClient.Flows.LogoutDeviceAdminAsync(UserKey.Value, request); + } + + if (result.IsSuccess) + { + Snackbar.Add("Logged out of device.", Severity.Success); + if (result?.Value?.CurrentChain == true) + { + Nav.NavigateTo("/login"); + return; + } + await ReloadAsync(); + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed to logout.", Severity.Error); + } + } + + private async Task RevokeAllAsync() + { + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Sessions.RevokeAllMyChainsAsync(); + } + else + { + result = await UAuthClient.Sessions.RevokeAllUserChainsAsync(UserKey.Value); + } + + if (result.IsSuccess) + { + Snackbar.Add("Logged out of all devices.", Severity.Success); + + if (UserKey is null) + Nav.NavigateTo("/login"); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to logout.", Severity.Error); + } + } + + private async Task RevokeOthersAsync() + { + var result = await UAuthClient.Sessions.RevokeMyOtherChainsAsync(); + if (result.IsSuccess) + { + Snackbar.Add("Revoked all other devices.", Severity.Success); + await ReloadAsync(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to logout.", Severity.Error); + } + } + + private async Task RevokeChainAsync(SessionChainId chainId) + { + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Sessions.RevokeMyChainAsync(chainId); + } + else + { + result = await UAuthClient.Sessions.RevokeUserChainAsync(UserKey.Value, chainId); + } + + if (result.IsSuccess) + { + Snackbar.Add("Device revoked successfully.", Severity.Success); + + if (result?.Value?.CurrentChain == true) + { + Nav.NavigateTo("/login"); + return; + } + await ReloadAsync(); + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to logout.", Severity.Error); + } + } + + private async Task ShowChainDetailsAsync(SessionChainId chainId) + { + UAuthResult result; + + if (UserKey is null) + { + result = await UAuthClient.Sessions.GetMyChainDetailAsync(chainId); + } + else + { + result = await UAuthClient.Sessions.GetUserChainDetailAsync(UserKey.Value, chainId); + } + + if (result.IsSuccess) + { + _chainDetail = result.Value; + } + else + { + Snackbar.Add(result?.ErrorText ?? "Failed to fetch chain details.", Severity.Error); + _chainDetail = null; + } + } + + private void ClearDetail() + { + _chainDetail = null; + } + + private void Cancel() => MudDialog.Cancel(); +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UserDetailDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UserDetailDialog.razor new file mode 100644 index 00000000..28bb738d --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UserDetailDialog.razor @@ -0,0 +1,75 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Common +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject IDialogService DialogService +@inject ISnackbar Snackbar + + + + User Management + @_user?.UserKey.Value + + + + + + + + Display Name + @_user?.DisplayName + + + + Username + @_user?.UserName + + + + Email + @_user?.PrimaryEmail + + + + Phone + @_user?.PrimaryPhone + + + + Created + @_user?.CreatedAt?.ToLocalTime() + + + + Status + @_user?.Status + + + @foreach (var s in Enum.GetValues()) + { + @s + } + + Change + + + + + + + + Management + + Sessions + Profile + Identifiers + Credentials + Roles + + + + + + Close + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UserDetailDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UserDetailDialog.razor.cs new file mode 100644 index 00000000..e5bd3a08 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UserDetailDialog.razor.cs @@ -0,0 +1,100 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Common; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs; + +public partial class UserDetailDialog +{ + private UserView? _user; + private UserStatus _status; + + [CascadingParameter] + IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + [Parameter] + public UserKey UserKey { get; set; } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + var result = await UAuthClient.Users.GetProfileAsync(UserKey); + + if (result.IsSuccess) + { + _user = result.Value; + _status = _user?.Status ?? UserStatus.Unknown; + } + } + + private async Task OpenSessions() + { + await DialogService.ShowAsync("Session Management", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenProfile() + { + await DialogService.ShowAsync("Profile Management", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenIdentifiers() + { + await DialogService.ShowAsync("Identifier Management", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenCredentials() + { + await DialogService.ShowAsync("Credentials", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenRoles() + { + await DialogService.ShowAsync("Roles", UAuthDialog.GetDialogParameters(AuthState, _user?.UserKey), UAuthDialog.GetDialogOptions()); + } + + private async Task ChangeStatusAsync() + { + if (_user is null) + return; + + ChangeUserStatusAdminRequest request = new() + { + NewStatus = _status + }; + + var result = await UAuthClient.Users.ChangeStatusAdminAsync(_user.UserKey, request); + + if (result.IsSuccess) + { + Snackbar.Add("User status updated", Severity.Success); + _user = _user with { Status = _status }; + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed", Severity.Error); + } + } + + private Color GetStatusColor(UserStatus? status) + { + return status switch + { + UserStatus.Active => Color.Success, + UserStatus.Suspended => Color.Warning, + UserStatus.Disabled => Color.Error, + _ => Color.Default + }; + } + + private void Close() + { + MudDialog.Close(); + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UserRoleDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UserRoleDialog.razor new file mode 100644 index 00000000..6e754848 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UserRoleDialog.razor @@ -0,0 +1,49 @@ +@using CodeBeam.UltimateAuth.Authorization.Contracts +@inject IUAuthClient UAuthClient +@inject IDialogService DialogService +@inject ISnackbar Snackbar + + + + + User Roles + UserKey: @UserKey.Value + + + + + Assigned Roles + + @if (_roles.Count == 0) + { + No roles assigned + } + + + @foreach (var role in _roles) + { + @role + } + + + + + Add Role + + + + @foreach (var role in _allRoles) + { + @role.Name + } + + + Add + + + + + + Close + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UserRoleDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UserRoleDialog.razor.cs new file mode 100644 index 00000000..94ec688a --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UserRoleDialog.razor.cs @@ -0,0 +1,112 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Domain; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs; + +public partial class UserRoleDialog +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + [Parameter] + public UserKey UserKey { get; set; } = default!; + + private List _roles = new(); + private List _allRoles = new(); + + private string? _selectedRole; + + protected override async Task OnInitializedAsync() + { + await LoadRoles(); + } + + private async Task LoadRoles() + { + var userRoles = await UAuthClient.Authorization.GetUserRolesAsync(UserKey); + + if (userRoles.IsSuccess && userRoles.Value != null) + _roles = userRoles.Value.Roles.Items.Select(x => x.Name).ToList(); + + var roles = await UAuthClient.Authorization.QueryRolesAsync(new RoleQuery + { + PageNumber = 1, + PageSize = 200 + }); + + if (roles.IsSuccess && roles.Value != null) + _allRoles = roles.Value.Items.ToList(); + } + + private async Task AddRole() + { + if (string.IsNullOrWhiteSpace(_selectedRole)) + return; + + var result = await UAuthClient.Authorization.AssignRoleToUserAsync(UserKey, _selectedRole); + + if (result.IsSuccess) + { + _roles.Add(_selectedRole); + Snackbar.Add("Role assigned", Severity.Success); + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed", Severity.Error); + } + + _selectedRole = null; + } + + private async Task RemoveRole(string role) + { + var confirm = await DialogService.ShowMessageBoxAsync( + "Remove Role", + $"Remove {role} from user?", + yesText: "Remove", + noText: "Cancel", + options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" }); + + if (confirm != true) + { + Snackbar.Add("Role remove process cancelled.", Severity.Info); + return; + } + + if (role == "Admin") + { + var confirm2 = await DialogService.ShowMessageBoxAsync( + "Are You Sure", + "You are going to remove admin role. This action may cause the application unuseable.", + yesText: "Remove", + noText: "Cancel", + options: new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, BackgroundClass = "uauth-blur-slight" }); + + if (confirm2 != true) + { + Snackbar.Add("Role remove process cancelled.", Severity.Info); + return; + } + } + + var result = await UAuthClient.Authorization.RemoveRoleFromUserAsync(UserKey, role); + + if (result.IsSuccess) + { + _roles.Remove(role); + Snackbar.Add("Role removed.", Severity.Success); + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed", Severity.Error); + } + } + + private void Close() => MudDialog.Close(); +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UsersDialog.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UsersDialog.razor new file mode 100644 index 00000000..5fba5e4c --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UsersDialog.razor @@ -0,0 +1,85 @@ +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Common +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient +@inject IDialogService DialogService +@inject ISnackbar Snackbar + + + + User Management + Browse, create and manage users + + + + + + + + + + + + + + + + + Users + + New User + + + + + + + + + + + @context.Item.Status + + + + + + + + + + + + + + + + + + + + + Id + @context.Item.UserKey.Value + + + + Created At + @context.Item.CreatedAt + + + + + + + + + + + + + Close + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UsersDialog.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UsersDialog.razor.cs new file mode 100644 index 00000000..2344bdf8 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Dialogs/UsersDialog.razor.cs @@ -0,0 +1,176 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Common; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs; + +public partial class UsersDialog +{ + private MudDataGrid? _grid; + private bool _loading; + private string? _search; + private bool _reloadQueued; + private UserStatus? _statusFilter; + + [CascadingParameter] + IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public UAuthState AuthState { get; set; } = default!; + + private async Task> LoadUsers(GridState state, CancellationToken ct) + { + var sort = state.SortDefinitions?.FirstOrDefault(); + + var req = new UserQuery + { + PageNumber = state.Page + 1, + PageSize = state.PageSize, + Search = _search, + Status = _statusFilter, + SortBy = sort?.SortBy, + Descending = sort?.Descending ?? false + }; + + var res = await UAuthClient.Users.QueryUsersAsync(req); + + if (!res.IsSuccess || res.Value == null) + { + Snackbar.Add(res.ErrorText ?? "Failed to load users.", Severity.Error); + + return new GridData + { + Items = Array.Empty(), + TotalItems = 0 + }; + } + + return new GridData + { + Items = res.Value.Items, + TotalItems = res.Value.TotalCount + }; + } + + private async Task ReloadAsync() + { + if (_loading) + { + _reloadQueued = true; + return; + } + + if (_grid is null) + return; + + _loading = true; + await InvokeAsync(StateHasChanged); + + try + { + await _grid.ReloadServerData(); + } + finally + { + _loading = false; + + if (_reloadQueued) + { + _reloadQueued = false; + await ReloadAsync(); + } + + await InvokeAsync(StateHasChanged); + } + } + + private async Task OnStatusChanged(UserStatus? status) + { + _statusFilter = status; + await ReloadAsync(); + } + + private async Task OpenUser(UserKey userKey) + { + var dialog = await DialogService.ShowAsync("User", UAuthDialog.GetDialogParameters(AuthState, userKey), UAuthDialog.GetDialogOptions()); + await dialog.Result; + await ReloadAsync(); + } + + private async Task OpenCreateUser() + { + var dialog = await DialogService.ShowAsync( + "Create User", + new DialogOptions + { + MaxWidth = MaxWidth.Small, + FullWidth = true, + CloseButton = true + }); + + var result = await dialog.Result; + + if (result?.Canceled == false) + await ReloadAsync(); + } + + private async Task DeleteUserAsync(UserSummary user) + { + var confirm = await DialogService.ShowMessageBoxAsync( + title: "Delete user", + markupMessage: (MarkupString)$""" + Are you sure you want to delete {user.DisplayName ?? user.UserName ?? user.PrimaryEmail ?? user.UserKey}? +

+ This operation is intended for admin usage. + """, + yesText: "Delete", + cancelText: "Cancel", + options: new DialogOptions + { + MaxWidth = MaxWidth.Medium, + FullWidth = true, + BackgroundClass = "uauth-blur-slight" + }); + + if (confirm != true) + return; + + var req = new DeleteUserRequest + { + Mode = DeleteMode.Soft + }; + + var result = await UAuthClient.Users.DeleteUserAsync(UserKey.Parse(user.UserKey, null), req); + + if (result.IsSuccess) + { + Snackbar.Add("User deleted successfully.", Severity.Success); + await ReloadAsync(); + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed to delete user.", Severity.Error); + } + } + + private static Color GetStatusColor(UserStatus status) + { + return status switch + { + UserStatus.Active => Color.Success, + UserStatus.SelfSuspended => Color.Warning, + UserStatus.Suspended => Color.Warning, + UserStatus.Disabled => Color.Error, + _ => Color.Default + }; + } + + private void Close() + { + MudDialog.Close(); + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/MainLayout.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/MainLayout.razor new file mode 100644 index 00000000..2e4a24bb --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/MainLayout.razor @@ -0,0 +1,65 @@ +@inherits LayoutComponentBase +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject NavigationManager Nav + + + + + UltimateAuth + + Blazor Server EFCore Sample + + + + + + + + + +
+ + + @((state.Identity?.DisplayName ?? "?").Trim() is var n ? (n.Length >= 2 ? n[..2] : n[..1]) : "?") + + +
+
+ + + @state.Identity?.DisplayName + @string.Join(", ", state.Claims.Roles) + + + + + + + + @if (state.Identity?.SessionState is not null && state.Identity.SessionState != SessionState.Active) + { + + + } + +
+
+ + + + +
+
+ + + @Body + +
+ + +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/MainLayout.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/MainLayout.razor.cs new file mode 100644 index 00000000..8ae19aa7 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/MainLayout.razor.cs @@ -0,0 +1,130 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Errors; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Infrastructure; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Layout; + +public partial class MainLayout +{ + [CascadingParameter] + public UAuthState UAuth { get; set; } = default!; + + [CascadingParameter] + public DarkModeManager DarkModeManager { get; set; } = default!; + + private async Task Refresh() + { + await UAuthClient.Flows.RefreshAsync(); + } + + private async Task Logout() + { + await UAuthClient.Flows.LogoutAsync(); + } + + private Color GetBadgeColor() + { + if (UAuth is null || !UAuth.IsAuthenticated) + return Color.Error; + + if (UAuth.IsStale) + return Color.Warning; + + var state = UAuth.Identity?.SessionState; + + if (state is null || state == SessionState.Active) + return Color.Success; + + if (state == SessionState.Invalid) + return Color.Error; + + return Color.Warning; + } + + private void HandleSignInClick() + { + var uri = Nav.ToAbsoluteUri(Nav.Uri); + + if (uri.AbsolutePath.EndsWith("/login", StringComparison.OrdinalIgnoreCase)) + { + Nav.NavigateTo("/login?focus=1", replace: true, forceLoad: true); + return; + } + + GoToLoginWithReturn(); + } + + private async Task Validate() + { + try + { + var result = await UAuthClient.Flows.ValidateAsync(); + + if (result.IsValid) + { + if (result.Snapshot?.Identity.UserStatus == UserStatus.SelfSuspended) + { + Snackbar.Add("Your account is suspended by you.", Severity.Warning); + return; + } + Snackbar.Add($"Session active • Tenant: {result.Snapshot?.Identity?.Tenant.Value} • User: {result.Snapshot?.Identity?.PrimaryUserName}", Severity.Success); + } + else + { + switch (result.State) + { + case SessionState.Expired: + Snackbar.Add("Session expired. Please sign in again.", Severity.Warning); + break; + + case SessionState.DeviceMismatch: + Snackbar.Add("Session invalid for this device.", Severity.Error); + break; + + default: + Snackbar.Add($"Session state: {result.State}", Severity.Error); + break; + } + } + } + catch (UAuthTransportException) + { + Snackbar.Add("Network error.", Severity.Error); + } + catch (UAuthProtocolException) + { + Snackbar.Add("Invalid response.", Severity.Error); + } + catch (UAuthException ex) + { + Snackbar.Add($"UAuth error: {ex.Message}", Severity.Error); + } + catch (Exception ex) + { + Snackbar.Add($"Unexpected error: {ex.Message}", Severity.Error); + } + } + + private void GoToLoginWithReturn() + { + var uri = Nav.ToAbsoluteUri(Nav.Uri); + + if (uri.AbsolutePath.EndsWith("/login", StringComparison.OrdinalIgnoreCase)) + { + Nav.NavigateTo("/login", replace: true); + return; + } + + var current = Nav.ToBaseRelativePath(uri.ToString()); + if (string.IsNullOrWhiteSpace(current)) + current = "home"; + + var returnUrl = Uri.EscapeDataString("/" + current.TrimStart('/')); + Nav.NavigateTo($"/login?returnUrl={returnUrl}", replace: true); + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/MainLayout.razor.css b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/MainLayout.razor.css new file mode 100644 index 00000000..df8c10ff --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/MainLayout.razor.css @@ -0,0 +1,18 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/ReconnectModal.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/ReconnectModal.razor new file mode 100644 index 00000000..e740b0c8 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/ReconnectModal.razor @@ -0,0 +1,31 @@ + + + +
+ +

+ Rejoining the server... +

+

+ Rejoin failed... trying again in seconds. +

+

+ Failed to rejoin.
Please retry or reload the page. +

+ +

+ The session has been paused by the server. +

+

+ Failed to resume the session.
Please retry or reload the page. +

+ +
+
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/ReconnectModal.razor.css b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/ReconnectModal.razor.css new file mode 100644 index 00000000..3ad3773f --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/ReconnectModal.razor.css @@ -0,0 +1,157 @@ +.components-reconnect-first-attempt-visible, +.components-reconnect-repeated-attempt-visible, +.components-reconnect-failed-visible, +.components-pause-visible, +.components-resume-failed-visible, +.components-rejoining-animation { + display: none; +} + +#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible, +#components-reconnect-modal.components-reconnect-show .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-paused .components-pause-visible, +#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible, +#components-reconnect-modal.components-reconnect-retrying, +#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible, +#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-failed, +#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { + display: block; +} + + +#components-reconnect-modal { + background-color: white; + width: 20rem; + margin: 20vh auto; + padding: 2rem; + border: 0; + border-radius: 0.5rem; + box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3); + opacity: 0; + transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; + animation: components-reconnect-modal-fadeOutOpacity 0.5s both; + &[open] + +{ + animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; + animation-fill-mode: both; +} + +} + +#components-reconnect-modal::backdrop { + background-color: rgba(0, 0, 0, 0.4); + animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; + opacity: 1; +} + +@keyframes components-reconnect-modal-slideUp { + 0% { + transform: translateY(30px) scale(0.95); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes components-reconnect-modal-fadeInOpacity { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes components-reconnect-modal-fadeOutOpacity { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +.components-reconnect-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +#components-reconnect-modal p { + margin: 0; + text-align: center; +} + +#components-reconnect-modal button { + border: 0; + background-color: #6b9ed2; + color: white; + padding: 4px 24px; + border-radius: 4px; +} + + #components-reconnect-modal button:hover { + background-color: #3b6ea2; + } + + #components-reconnect-modal button:active { + background-color: #6b9ed2; + } + +.components-rejoining-animation { + position: relative; + width: 80px; + height: 80px; +} + + .components-rejoining-animation div { + position: absolute; + border: 3px solid #0087ff; + opacity: 1; + border-radius: 50%; + animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; + } + + .components-rejoining-animation div:nth-child(2) { + animation-delay: -0.5s; + } + +@keyframes components-rejoining-animation { + 0% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 4.9% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 5% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 1; + } + + 100% { + top: 0px; + left: 0px; + width: 80px; + height: 80px; + opacity: 0; + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/ReconnectModal.razor.js b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/ReconnectModal.razor.js new file mode 100644 index 00000000..a44de78d --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Layout/ReconnectModal.razor.js @@ -0,0 +1,63 @@ +// Set up event handlers +const reconnectModal = document.getElementById("components-reconnect-modal"); +reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged); + +const retryButton = document.getElementById("components-reconnect-button"); +retryButton.addEventListener("click", retry); + +const resumeButton = document.getElementById("components-resume-button"); +resumeButton.addEventListener("click", resume); + +function handleReconnectStateChanged(event) { + if (event.detail.state === "show") { + reconnectModal.showModal(); + } else if (event.detail.state === "hide") { + reconnectModal.close(); + } else if (event.detail.state === "failed") { + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } else if (event.detail.state === "rejected") { + location.reload(); + } +} + +async function retry() { + document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + + try { + // Reconnect will asynchronously return: + // - true to mean success + // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) + // - exception to mean we didn't reach the server (this can be sync or async) + const successful = await Blazor.reconnect(); + if (!successful) { + // We have been able to reach the server, but the circuit is no longer available. + // We'll reload the page so the user can continue using the app as quickly as possible. + const resumeSuccessful = await Blazor.resumeCircuit(); + if (!resumeSuccessful) { + location.reload(); + } else { + reconnectModal.close(); + } + } + } catch (err) { + // We got an exception, server is currently unavailable + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } +} + +async function resume() { + try { + const successful = await Blazor.resumeCircuit(); + if (!successful) { + location.reload(); + } + } catch { + reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed"); + } +} + +async function retryWhenDocumentBecomesVisible() { + if (document.visibilityState === "visible") { + await retry(); + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/AnonymousTestPage.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/AnonymousTestPage.razor new file mode 100644 index 00000000..10d035ba --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/AnonymousTestPage.razor @@ -0,0 +1 @@ +@page "/anonymous-test" diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/AuthorizedTestPage.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/AuthorizedTestPage.razor new file mode 100644 index 00000000..5dc5d8aa --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/AuthorizedTestPage.razor @@ -0,0 +1,26 @@ +@page "/authorized-test" +@attribute [Authorize] + + + + + + + Everything is Ok + + + If you see this section, it means you succesfully logged in. + + + + Go Profile + + + + + + UltimateAuth protects this resource based on your session and permissions. + + + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Error.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Error.razor new file mode 100644 index 00000000..576cc2d2 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Home.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Home.razor new file mode 100644 index 00000000..76de9054 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Home.razor @@ -0,0 +1,444 @@ +@page "/home" +@attribute [Authorize] +@inherits UAuthFlowPageBase + +@inject IUAuthClient UAuthClient +@inject UAuthClientDiagnostics Diagnostics +@inject AuthenticationStateProvider AuthStateProvider +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@using System.Security.Claims +@using CodeBeam.UltimateAuth.Core.Contracts +@using CodeBeam.UltimateAuth.Core.Defaults +@using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Custom +@using Microsoft.AspNetCore.Authorization + +@if (AuthState?.Identity?.UserStatus == UserStatus.SelfSuspended) +{ + + + + Your account is suspended. Please active it before continue. + + + + Set Active + Logout + + + + return; +} + +@if (AuthState?.Identity?.UserStatus == UserStatus.Suspended) +{ + + + + Your account is suspended. Please contact with administrator. + + + + Logout + + + + return; +} + + + + + + + + + + + Session + + + + + + + Validate + + + + + + Manual Refresh + + + + + + Logout + + + + + + Account + + + + + Manage Sessions + + + + Manage Profile + + + + Manage Identifiers + + + + Manage Credentials + + + + Suspend | Delete Account + + + + Admin + + + + + + + + + @if (_showAdminPreview) + { + + Admin operations are shown for preview. Sign in as an Admin to execute them. + + } + + @if (AuthState?.IsInRole("Admin") == true || _showAdminPreview) + { + + + + @* *@ + @* *@ + User Management + @* *@ + + + + + + @* *@ + Role Management + @* *@ + + + + } + + + + + + + + + + + @((AuthState?.Identity?.DisplayName ?? "?").Substring(0, Math.Min(2, (AuthState?.Identity?.DisplayName ?? "?").Length))) + + + + @AuthState?.Identity?.DisplayName + + @foreach (var role in AuthState?.Claims?.Roles ?? Enumerable.Empty()) + { + + @role + + } + + + + + + + + + + @if (_selectedAuthState == "UAuthState") + { + + +
+ + + Tenant + + @AuthState?.Identity?.Tenant.Value +
+ +
+ + +
+ + + User Id + + @AuthState?.Identity?.UserKey.Value +
+
+ + +
+ + + Authenticated + + @(AuthState?.IsAuthenticated == true ? "Yes" : "No") +
+
+ + +
+ + + Session State + + @AuthState?.Identity?.SessionState?.ToDescriptionString() +
+
+ + +
+ + + Username + + @AuthState?.Identity?.PrimaryUserName +
+
+ + +
+ + + Display Name + + @AuthState?.Identity?.DisplayName +
+
+ + + + + + + Email + + @AuthState?.Identity?.PrimaryEmail + + + + + + Phone + + @AuthState?.Identity?.PrimaryPhone + + + + + + + + Authenticated At + + @* TODO: Add IUAuthDateTimeFormatter *@ + @FormatLocalTime(AuthState?.Identity?.AuthenticatedAt) + + + + + + Last Validated At + + @* TODO: Validation call should update last validated at *@ + @FormatLocalTime(AuthState?.LastValidatedAt) + +
+ } + else if (_selectedAuthState == "AspNetCoreState") + { + + +
+ + + Authenticated + + @(_aspNetCoreState?.Identity?.IsAuthenticated == true ? "Yes" : "No") +
+
+ + +
+ + + User Id + + @_aspNetCoreState?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value +
+
+ + +
+ + + Username + + @_aspNetCoreState?.Identity?.Name +
+
+ + +
+ + + Authentication Type + + @_aspNetCoreState?.Identity?.AuthenticationType +
+
+
+ } +
+
+
+ + + + + + @GetHealthText() + + + Lifecycle + + + + + + Started + @Diagnostics.StartCount + + @if (Diagnostics.StartedAt is not null) + { + + + + @FormatRelative(Diagnostics.StartedAt) + + + } + + + + + Stopped + @Diagnostics.StopCount + + + + + + Terminated + @Diagnostics.TerminatedCount + + @if (Diagnostics.TerminatedAt is not null) + { + + + + + @FormatRelative(Diagnostics.TerminatedAt) + + + + } + + + + + + Refresh Metrics + + + + + + + Total Attempts + @Diagnostics.RefreshAttemptCount + + + + + + + Success + + @Diagnostics.RefreshSuccessCount + + + + + + Automatic + @Diagnostics.AutomaticRefreshCount + + + + + + Manual + @Diagnostics.ManualRefreshCount + + + + + + Touched/Rotated + @Diagnostics.RefreshTouchedCount / @Diagnostics.RefreshRotatedCount + + + + + + No-Op + @Diagnostics.RefreshNoOpCount + + + + + + Reauth Required + @Diagnostics.RefreshReauthRequiredCount + + + + + + + +
+
+
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Home.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Home.razor.cs new file mode 100644 index 00000000..c5d1da90 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Home.razor.cs @@ -0,0 +1,222 @@ +using CodeBeam.UltimateAuth.Client.Blazor; +using CodeBeam.UltimateAuth.Client.Errors; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Common; +using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.AspNetCore.Components.Authorization; +using MudBlazor; +using System.Security.Claims; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Pages; + +public partial class Home : UAuthFlowPageBase +{ + private string _selectedAuthState = "UAuthState"; + private ClaimsPrincipal? _aspNetCoreState; + + private bool _showAdminPreview = false; + + protected override async Task OnInitializedAsync() + { + var initial = await AuthStateProvider.GetAuthenticationStateAsync(); + _aspNetCoreState = initial.User; + AuthStateProvider.AuthenticationStateChanged += OnAuthStateChanged; + Diagnostics.Changed += OnDiagnosticsChanged; + } + + private void OnAuthStateChanged(Task task) + { + _ = HandleAuthStateChangedAsync(task); + } + + private async Task HandleAuthStateChangedAsync(Task task) + { + try + { + var state = await task; + _aspNetCoreState = state.User; + await InvokeAsync(StateHasChanged); + } + catch + { + + } + } + + private void OnDiagnosticsChanged() + { + InvokeAsync(StateHasChanged); + } + + private async Task Logout() => await UAuthClient.Flows.LogoutAsync(); + + private async Task RefreshSession() => await UAuthClient.Flows.RefreshAsync(false); + + private async Task Validate() + { + try + { + var result = await UAuthClient.Flows.ValidateAsync(); + + if (result.IsValid) + { + if (result.Snapshot?.Identity.UserStatus == UserStatus.SelfSuspended) + { + Snackbar.Add("Your account is suspended by you.", Severity.Warning); + return; + } + Snackbar.Add($"Session active • Tenant: {result.Snapshot?.Identity?.Tenant.Value} • User: {result.Snapshot?.Identity?.PrimaryUserName}", Severity.Success); + } + else + { + switch (result.State) + { + case SessionState.Expired: + Snackbar.Add("Session expired. Please sign in again.", Severity.Warning); + break; + + case SessionState.DeviceMismatch: + Snackbar.Add("Session invalid for this device.", Severity.Error); + break; + + default: + Snackbar.Add($"Session state: {result.State}", Severity.Error); + break; + } + } + } + catch (UAuthTransportException) + { + Snackbar.Add("Network error.", Severity.Error); + } + catch (UAuthProtocolException) + { + Snackbar.Add("Invalid response.", Severity.Error); + } + catch (UAuthException ex) + { + Snackbar.Add($"UAuth error: {ex.Message}", Severity.Error); + } + catch (Exception ex) + { + Snackbar.Add($"Unexpected error: {ex.Message}", Severity.Error); + } + } + + private Color GetHealthColor() + { + if (Diagnostics.RefreshReauthRequiredCount > 0) + return Color.Warning; + + if (Diagnostics.TerminatedCount > 0) + return Color.Error; + + return Color.Success; + } + + private string GetHealthText() + { + if (Diagnostics.RefreshReauthRequiredCount > 0) + return "Reauthentication Required"; + + if (Diagnostics.TerminatedCount > 0) + return "Session Terminated"; + + return "Healthy"; + } + + private string? FormatRelative(DateTimeOffset? utc) + { + if (utc is null) + return null; + + var diff = DateTimeOffset.UtcNow - utc.Value; + + if (diff.TotalSeconds < 5) + return "just now"; + + if (diff.TotalSeconds < 60) + return $"{(int)diff.Seconds} secs ago"; + + if (diff.TotalMinutes < 60) + return $"{(int)diff.TotalMinutes} min ago"; + + if (diff.TotalHours < 24) + return $"{(int)diff.TotalHours} hrs ago"; + + return utc.Value.ToLocalTime().ToString("dd MMM yyyy"); + } + + private string? FormatLocalTime(DateTimeOffset? utc) + { + return utc?.ToLocalTime().ToString("dd MMM yyyy • HH:mm:ss"); + } + + private async Task OpenProfileDialog() + { + await DialogService.ShowAsync("Manage Profile", GetDialogParameters(), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenIdentifierDialog() + { + await DialogService.ShowAsync("Manage Identifiers", GetDialogParameters(), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenSessionDialog() + { + await DialogService.ShowAsync("Manage Sessions", GetDialogParameters(), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenCredentialDialog() + { + await DialogService.ShowAsync("Session Diagnostics", GetDialogParameters(), UAuthDialog.GetDialogOptions()); + } + + private async Task OpenAccountStatusDialog() + { + await DialogService.ShowAsync("Manage Account", GetDialogParameters(), UAuthDialog.GetDialogOptions(MaxWidth.ExtraSmall)); + } + + private async Task OpenUserDialog() + { + await DialogService.ShowAsync("User Management", GetDialogParameters(), UAuthDialog.GetDialogOptions(MaxWidth.Large)); + } + + private async Task OpenRoleDialog() + { + await DialogService.ShowAsync("Role Management", GetDialogParameters(), UAuthDialog.GetDialogOptions()); + } + + private DialogParameters GetDialogParameters() + { + return new DialogParameters + { + ["AuthState"] = AuthState + }; + } + + private async Task SetAccountActiveAsync() + { + ChangeUserStatusSelfRequest request = new() { NewStatus = SelfUserStatus.Active }; + var result = await UAuthClient.Users.ChangeStatusSelfAsync(request); + + if (result.IsSuccess) + { + Snackbar.Add("Account activated successfully.", Severity.Success); + } + else + { + Snackbar.Add(result?.Problem?.Detail ?? result?.Problem?.Title ?? "Activation failed.", Severity.Error); + } + } + + public override void Dispose() + { + base.Dispose(); + AuthStateProvider.AuthenticationStateChanged -= OnAuthStateChanged; + Diagnostics.Changed -= OnDiagnosticsChanged; + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/LandingPage.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/LandingPage.razor new file mode 100644 index 00000000..1e4a9016 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/LandingPage.razor @@ -0,0 +1,4 @@ +@page "/" + +@inject NavigationManager Nav +@inject AuthenticationStateProvider AuthProvider diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/LandingPage.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/LandingPage.razor.cs new file mode 100644 index 00000000..5733e2eb --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/LandingPage.razor.cs @@ -0,0 +1,17 @@ +using CodeBeam.UltimateAuth.Core.Defaults; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Pages; + +public partial class LandingPage +{ + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + var state = await AuthProvider.GetAuthenticationStateAsync(); + var isAuthenticated = state.User.Identity?.IsAuthenticated == true; + + Nav.NavigateTo(isAuthenticated ? "/home" : $"{UAuthConstants.Routes.LoginRedirect}?fresh=true"); + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Login.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Login.razor new file mode 100644 index 00000000..f1d587c7 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Login.razor @@ -0,0 +1,126 @@ +@page "/login" +@attribute [UAuthLoginPage] +@inherits UAuthFlowPageBase + +@implements IDisposable +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IUAuthClientProductInfoProvider ClientProductInfoProvider +@inject IDeviceIdProvider DeviceIdProvider +@inject IDialogService DialogService + + + + + + + + + + + + + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Login.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Login.razor.cs new file mode 100644 index 00000000..0559a29b --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Login.razor.cs @@ -0,0 +1,211 @@ +using CodeBeam.UltimateAuth.Client.Blazor; +using CodeBeam.UltimateAuth.Client.Runtime; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Dialogs; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Pages; + +public partial class Login : UAuthFlowPageBase +{ + private string? _username; + private string? _password; + private UAuthClientProductInfo? _productInfo; + private MudTextField _usernameField = default!; + + private CancellationTokenSource? _lockoutCts; + private PeriodicTimer? _lockoutTimer; + private DateTimeOffset? _lockoutUntil; + private TimeSpan _remaining; + private bool _isLocked; + private DateTimeOffset? _lockoutStartedAt; + private TimeSpan _lockoutDuration; + private double _progressPercent; + private int? _remainingAttempts = null; + + protected override async Task OnInitializedAsync() + { + _productInfo = ClientProductInfoProvider.Get(); + } + + protected override Task OnUAuthPayloadAsync(AuthFlowPayload payload) + { + HandleLoginPayload(payload); + return Task.CompletedTask; + } + + protected override async Task OnFocusRequestedAsync() + { + await _usernameField.FocusAsync(); + } + + private void HandleLoginPayload(AuthFlowPayload payload) + { + if (payload.Flow != AuthFlowType.Login) + return; + + if (payload.Reason == AuthFailureReason.LockedOut && payload.LockoutUntilUtc is { } until) + { + _lockoutUntil = until; + StartCountdown(); + } + + _remainingAttempts = payload.RemainingAttempts; + ShowLoginError(payload.Reason, payload.RemainingAttempts); + } + + private void ShowLoginError(AuthFailureReason? reason, int? remainingAttempts) + { + string message = reason switch + { + AuthFailureReason.InvalidCredentials when remainingAttempts is > 0 + => $"Invalid username or password. {remainingAttempts} attempt(s) remaining.", + + AuthFailureReason.InvalidCredentials + => "Invalid username or password.", + + AuthFailureReason.RequiresMfa + => "Multi-factor authentication required.", + + AuthFailureReason.LockedOut + => "Your account is locked.", + + _ => "Login failed." + }; + + Snackbar.Add(message, Severity.Error); + } + + private async Task ProgrammaticLogin() + { + var request = new LoginRequest + { + Identifier = "admin", + Secret = "admin", + }; + await UAuthClient.Flows.LoginAsync(request, ReturnUrl ?? "/home"); + } + + private async void StartCountdown() + { + if (_lockoutUntil is null) + return; + + _isLocked = true; + _lockoutStartedAt = DateTimeOffset.UtcNow; + _lockoutDuration = _lockoutUntil.Value - DateTimeOffset.UtcNow; + UpdateRemaining(); + + _lockoutCts?.Cancel(); + _lockoutCts = new CancellationTokenSource(); + + _lockoutTimer?.Dispose(); + _lockoutTimer = new PeriodicTimer(TimeSpan.FromSeconds(1)); + + try + { + while (await _lockoutTimer.WaitForNextTickAsync(_lockoutCts.Token)) + { + UpdateRemaining(); + + if (_remaining <= TimeSpan.Zero) + { + ResetLockoutState(); + await InvokeAsync(StateHasChanged); + break; + } + + await InvokeAsync(StateHasChanged); + } + } + catch (OperationCanceledException) + { + + } + } + + private void ResetLockoutState() + { + _isLocked = false; + _lockoutUntil = null; + _progressPercent = 0; + _remainingAttempts = null; + } + + private void UpdateRemaining() + { + if (_lockoutUntil is null || _lockoutStartedAt is null) + return; + + var now = DateTimeOffset.UtcNow; + + _remaining = _lockoutUntil.Value - now; + + if (_remaining <= TimeSpan.Zero) + { + _remaining = TimeSpan.Zero; + return; + } + + var elapsed = now - _lockoutStartedAt.Value; + + if (_lockoutDuration.TotalSeconds > 0) + { + var percent = 100 - (elapsed.TotalSeconds / _lockoutDuration.TotalSeconds * 100); + _progressPercent = Math.Max(0, percent); + } + } + + private void HandleTry(IUAuthTryResult result) + { + if (result is TryLoginResult pkce) + { + if (!result.Success) + { + if (result.Reason == AuthFailureReason.LockedOut && result.LockoutUntilUtc is { } until) + { + _lockoutUntil = until; + StartCountdown(); + } + + _remainingAttempts = result.RemainingAttempts; + ShowLoginError(result.Reason, result.RemainingAttempts); + } + } + else + { + Snackbar.Add("Unexpected result type.", Severity.Error); + } + } + + private async Task OpenResetDialog() + { + await DialogService.ShowAsync("Reset Credentials", GetDialogParameters(), GetDialogOptions()); + } + + private DialogOptions GetDialogOptions() + { + return new DialogOptions + { + MaxWidth = MaxWidth.Medium, + FullWidth = true, + CloseButton = true + }; + } + + private DialogParameters GetDialogParameters() + { + return new DialogParameters + { + ["AuthState"] = AuthState + }; + } + + public override void Dispose() + { + base.Dispose(); + _lockoutCts?.Cancel(); + _lockoutTimer?.Dispose(); + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/NotAuthorized.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/NotAuthorized.razor new file mode 100644 index 00000000..d8eb7138 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/NotAuthorized.razor @@ -0,0 +1,27 @@ +@inject NavigationManager Nav + + + + + + + Access Denied + + + You don’t have permission to view this page. + If you think this is a mistake, sign in with a different account or request access. + + + + Sign In + Go Back + + + + + + UltimateAuth protects this resource based on your session and permissions. + + + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/NotAuthorized.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/NotAuthorized.razor.cs new file mode 100644 index 00000000..b38fbe97 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/NotAuthorized.razor.cs @@ -0,0 +1,15 @@ +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Pages; + +public partial class NotAuthorized +{ + private string LoginHref + { + get + { + var returnUrl = Uri.EscapeDataString(Nav.ToBaseRelativePath(Nav.Uri)); + return $"/login?returnUrl=/{returnUrl}"; + } + } + + private void GoBack() => Nav.NavigateTo("/", replace: false); +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Register.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Register.razor new file mode 100644 index 00000000..881cae5c --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Register.razor @@ -0,0 +1,60 @@ +@page "/register" +@inherits UAuthFlowPageBase + +@implements IDisposable +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar +@inject IUAuthClientProductInfoProvider ClientProductInfoProvider +@inject IDeviceIdProvider DeviceIdProvider +@inject IDialogService DialogService + + + + + + + + + + + + + + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Register.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Register.razor.cs new file mode 100644 index 00000000..e8c16205 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/Register.razor.cs @@ -0,0 +1,45 @@ +using CodeBeam.UltimateAuth.Client.Runtime; +using CodeBeam.UltimateAuth.Users.Contracts; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Pages; + +public partial class Register +{ + private string? _username; + private string? _password; + private string? _passwordCheck; + private string? _email; + private UAuthClientProductInfo? _productInfo; + private MudForm _form = null!; + + protected override async Task OnInitializedAsync() + { + _productInfo = ClientProductInfoProvider.Get(); + } + + private async Task HandleRegisterAsync() + { + await _form.Validate(); + + if (!_form.IsValid) + return; + + var request = new CreateUserRequest + { + UserName = _username, + Password = _password, + Email = _email, + }; + + var result = await UAuthClient.Users.CreateAsync(request); + if (result.IsSuccess) + { + Snackbar.Add("User created successfully.", Severity.Success); + } + else + { + Snackbar.Add(result.ErrorText ?? "Failed to create user.", Severity.Error); + } + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/ResetCredential.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/ResetCredential.razor new file mode 100644 index 00000000..753878b8 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/ResetCredential.razor @@ -0,0 +1,18 @@ +@page "/reset" +@inherits UAuthFlowPageBase + +@inject IUAuthClient UAuthClient +@inject ISnackbar Snackbar + + + + + + + + + Change Password + + + + \ No newline at end of file diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/ResetCredential.razor.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/ResetCredential.razor.cs new file mode 100644 index 00000000..3bdd9f68 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Pages/ResetCredential.razor.cs @@ -0,0 +1,49 @@ +using CodeBeam.UltimateAuth.Credentials.Contracts; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Pages; + +public partial class ResetCredential +{ + private MudForm _form = null!; + private string? _code; + private string? _newPassword; + private string? _newPasswordCheck; + + private async Task ResetPasswordAsync() + { + await _form.Validate(); + if (!_form.IsValid) + { + Snackbar.Add("Please fix the validation errors.", Severity.Error); + return; + } + + if (_newPassword != _newPasswordCheck) + { + Snackbar.Add("Passwords do not match.", Severity.Error); + return; + } + + var request = new CompleteCredentialResetRequest + { + ResetToken = _code, + NewSecret = _newPassword ?? string.Empty, + Identifier = Identifier // Coming from UAuthFlowPageBase automatically if begin reset is successful + }; + + var result = await UAuthClient.Credentials.CompleteResetMyAsync(request); + + if (result.IsSuccess) + { + Snackbar.Add("Credential reset successfully. Please log in with your new password.", Severity.Success); + Nav.NavigateTo("/login"); + } + else + { + Snackbar.Add(result.Problem?.Detail ?? result.Problem?.Title ?? "Failed to reset credential. Please try again.", Severity.Error); + } + } + + private string PasswordMatch(string arg) => _newPassword != arg ? "Passwords don't match" : string.Empty; +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Routes.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Routes.razor new file mode 100644 index 00000000..60331591 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/Routes.razor @@ -0,0 +1,73 @@ +@using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Pages +@using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Infrastructure +@inject ISnackbar Snackbar +@inject DarkModeManager DarkModeManager + + + + + + + + + + + + + + + + @* Advanced: you can fully control routing by providing your own Router (in the commented code below) *@ + @* + + + + + + + + + + + + + + + + *@ + + +@code { + private async Task HandleReauth() + { + Snackbar.Add("Reauthentication required. Please log in again.", Severity.Warning); + } + + #region DarkMode + + protected override void OnInitialized() + { + DarkModeManager.Changed += OnThemeChanged; + } + + private void OnThemeChanged() + { + InvokeAsync(StateHasChanged); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await DarkModeManager.InitializeAsync(); + StateHasChanged(); + } + } + + public void Dispose() + { + DarkModeManager.Changed -= OnThemeChanged; + } + + #endregion +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/_Imports.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/_Imports.razor new file mode 100644 index 00000000..59225b8c --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Components/_Imports.razor @@ -0,0 +1,23 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Authorization +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore +@using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components +@using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components.Layout + +@using CodeBeam.UltimateAuth.Core.Abstractions +@using CodeBeam.UltimateAuth.Core.Domain +@using CodeBeam.UltimateAuth.Client +@using CodeBeam.UltimateAuth.Client.Runtime +@using CodeBeam.UltimateAuth.Client.Diagnostics +@using CodeBeam.UltimateAuth.Client.Blazor + +@using MudBlazor +@using MudExtensions diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Infrastructure/DarkModeManager.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Infrastructure/DarkModeManager.cs new file mode 100644 index 00000000..befbcb29 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Infrastructure/DarkModeManager.cs @@ -0,0 +1,45 @@ +using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Infrastructure; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Infrastructure; + +public sealed class DarkModeManager +{ + private const string StorageKey = "uauth:theme:dark"; + + private readonly IClientStorage _storage; + + public DarkModeManager(IClientStorage storage) + { + _storage = storage; + } + + public async Task InitializeAsync() + { + var value = await _storage.GetAsync(StorageScope.Local, StorageKey); + + if (bool.TryParse(value, out var parsed)) + IsDarkMode = parsed; + } + + public bool IsDarkMode { get; set; } + + public event Action? Changed; + + public async Task ToggleAsync() + { + IsDarkMode = !IsDarkMode; + + await _storage.SetAsync(StorageScope.Local, StorageKey, IsDarkMode.ToString()); + Changed?.Invoke(); + } + + public void Set(bool value) + { + if (IsDarkMode == value) + return; + + IsDarkMode = value; + Changed?.Invoke(); + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260327184128_InitUltimateAuth.Designer.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260327184128_InitUltimateAuth.Designer.cs new file mode 100644 index 00000000..079580bb --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260327184128_InitUltimateAuth.Designer.cs @@ -0,0 +1,710 @@ +// +using System; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations +{ + [DbContext(typeof(UAuthDbContext))] + [Migration("20260327184128_InitUltimateAuth")] + partial class InitUltimateAuth + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CredentialType") + .HasColumnType("INTEGER"); + + b.Property("FailedAttempts") + .HasColumnType("INTEGER"); + + b.Property("LastFailedAt") + .HasColumnType("TEXT"); + + b.Property("LockedUntil") + .HasColumnType("TEXT"); + + b.Property("RequiresReauthentication") + .HasColumnType("INTEGER"); + + b.Property("ResetAttempts") + .HasColumnType("INTEGER"); + + b.Property("ResetConsumedAt") + .HasColumnType("TEXT"); + + b.Property("ResetExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ResetRequestedAt") + .HasColumnType("TEXT"); + + b.Property("ResetTokenHash") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Scope") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "LockedUntil"); + + b.HasIndex("Tenant", "ResetRequestedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "Scope"); + + b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") + .IsUnique(); + + b.ToTable("UAuth_Authentication", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "RoleId", "Permission"); + + b.HasIndex("Tenant", "Permission"); + + b.HasIndex("Tenant", "RoleId"); + + b.ToTable("UAuth_RolePermissions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "NormalizedName") + .IsUnique(); + + b.ToTable("UAuth_Roles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("AssignedAt") + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "UserKey", "RoleId"); + + b.HasIndex("Tenant", "RoleId"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserRoles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeletedAt"); + + b.ToTable("UAuth_PasswordCredentials", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ActiveSessionId") + .HasColumnType("TEXT"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("ClaimsSnapshot") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasColumnType("TEXT"); + + b.Property("RotationCount") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TouchCount") + .HasColumnType("INTEGER"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId") + .IsUnique(); + + b.HasIndex("Tenant", "RootId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeviceId"); + + b.ToTable("UAuth_SessionChains", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "SessionId") + .IsUnique(); + + b.HasIndex("Tenant", "ChainId", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_Sessions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "RootId") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.ToTable("UAuth_SessionRoots", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenId") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "ReplacedByTokenHash"); + + b.HasIndex("Tenant", "SessionId"); + + b.HasIndex("Tenant", "TokenHash") + .IsUnique(); + + b.HasIndex("Tenant", "TokenId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); + + b.HasIndex("Tenant", "TokenHash", "RevokedAt"); + + b.ToTable("UAuth_RefreshTokens", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("NormalizedValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("VerifiedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "NormalizedValue"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "Type", "NormalizedValue") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey", "IsPrimary"); + + b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary"); + + b.ToTable("UAuth_UserIdentifiers", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.ToTable("UAuth_UserLifecycles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Bio") + .HasColumnType("TEXT"); + + b.Property("BirthDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Culture") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("Gender") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TimeZone") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserProfiles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) + .WithMany() + .HasForeignKey("Tenant", "RootId") + .HasPrincipalKey("Tenant", "RootId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) + .WithMany() + .HasForeignKey("Tenant", "ChainId") + .HasPrincipalKey("Tenant", "ChainId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260327184128_InitUltimateAuth.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260327184128_InitUltimateAuth.cs new file mode 100644 index 00000000..9f373138 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20260327184128_InitUltimateAuth.cs @@ -0,0 +1,556 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations +{ + /// + public partial class InitUltimateAuth : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UAuth_Authentication", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Scope = table.Column(type: "INTEGER", nullable: false), + CredentialType = table.Column(type: "INTEGER", nullable: true), + FailedAttempts = table.Column(type: "INTEGER", nullable: false), + LastFailedAt = table.Column(type: "TEXT", nullable: true), + LockedUntil = table.Column(type: "TEXT", nullable: true), + RequiresReauthentication = table.Column(type: "INTEGER", nullable: false), + ResetRequestedAt = table.Column(type: "TEXT", nullable: true), + ResetExpiresAt = table.Column(type: "TEXT", nullable: true), + ResetConsumedAt = table.Column(type: "TEXT", nullable: true), + ResetTokenHash = table.Column(type: "TEXT", maxLength: 512, nullable: true), + ResetAttempts = table.Column(type: "INTEGER", nullable: false), + SecurityVersion = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_Authentication", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UAuth_PasswordCredentials", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + SecretHash = table.Column(type: "TEXT", maxLength: 512, nullable: false), + RevokedAt = table.Column(type: "TEXT", nullable: true), + ExpiresAt = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(type: "TEXT", nullable: false), + LastUsedAt = table.Column(type: "TEXT", nullable: true), + Source = table.Column(type: "TEXT", maxLength: 128, nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + DeletedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_PasswordCredentials", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UAuth_RefreshTokens", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TokenId = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + TokenHash = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + SessionId = table.Column(type: "TEXT", nullable: false), + ChainId = table.Column(type: "TEXT", nullable: true), + ReplacedByTokenHash = table.Column(type: "TEXT", maxLength: 128, nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + ExpiresAt = table.Column(type: "TEXT", nullable: false), + RevokedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_RefreshTokens", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UAuth_RolePermissions", + columns: table => new + { + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false), + Permission = table.Column(type: "TEXT", maxLength: 256, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_RolePermissions", x => new { x.Tenant, x.RoleId, x.Permission }); + }); + + migrationBuilder.CreateTable( + name: "UAuth_Roles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + NormalizedName = table.Column(type: "TEXT", maxLength: 128, nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + DeletedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_Roles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UAuth_SessionRoots", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RootId = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + RevokedAt = table.Column(type: "TEXT", nullable: true), + SecurityVersion = table.Column(type: "INTEGER", nullable: false), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_SessionRoots", x => x.Id); + table.UniqueConstraint("AK_UAuth_SessionRoots_Tenant_RootId", x => new { x.Tenant, x.RootId }); + }); + + migrationBuilder.CreateTable( + name: "UAuth_UserIdentifiers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Type = table.Column(type: "INTEGER", nullable: false), + Value = table.Column(type: "TEXT", maxLength: 256, nullable: false), + NormalizedValue = table.Column(type: "TEXT", maxLength: 256, nullable: false), + IsPrimary = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + VerifiedAt = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + DeletedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_UserIdentifiers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UAuth_UserLifecycles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + SecurityVersion = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + DeletedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_UserLifecycles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UAuth_UserProfiles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + FirstName = table.Column(type: "TEXT", nullable: true), + LastName = table.Column(type: "TEXT", nullable: true), + DisplayName = table.Column(type: "TEXT", nullable: true), + BirthDate = table.Column(type: "TEXT", nullable: true), + Gender = table.Column(type: "TEXT", nullable: true), + Bio = table.Column(type: "TEXT", nullable: true), + Language = table.Column(type: "TEXT", nullable: true), + TimeZone = table.Column(type: "TEXT", nullable: true), + Culture = table.Column(type: "TEXT", nullable: true), + Metadata = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + DeletedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_UserProfiles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UAuth_UserRoles", + columns: table => new + { + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false), + AssignedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_UserRoles", x => new { x.Tenant, x.UserKey, x.RoleId }); + }); + + migrationBuilder.CreateTable( + name: "UAuth_SessionChains", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ChainId = table.Column(type: "TEXT", nullable: false), + RootId = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + LastSeenAt = table.Column(type: "TEXT", nullable: false), + AbsoluteExpiresAt = table.Column(type: "TEXT", nullable: true), + DeviceId = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Device = table.Column(type: "TEXT", nullable: false), + ClaimsSnapshot = table.Column(type: "TEXT", nullable: false), + ActiveSessionId = table.Column(type: "TEXT", nullable: true), + RotationCount = table.Column(type: "INTEGER", nullable: false), + TouchCount = table.Column(type: "INTEGER", nullable: false), + SecurityVersionAtCreation = table.Column(type: "INTEGER", nullable: false), + RevokedAt = table.Column(type: "TEXT", nullable: true), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_SessionChains", x => x.Id); + table.UniqueConstraint("AK_UAuth_SessionChains_Tenant_ChainId", x => new { x.Tenant, x.ChainId }); + table.ForeignKey( + name: "FK_UAuth_SessionChains_UAuth_SessionRoots_Tenant_RootId", + columns: x => new { x.Tenant, x.RootId }, + principalTable: "UAuth_SessionRoots", + principalColumns: new[] { "Tenant", "RootId" }, + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "UAuth_Sessions", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + SessionId = table.Column(type: "TEXT", nullable: false), + ChainId = table.Column(type: "TEXT", nullable: false), + Tenant = table.Column(type: "TEXT", maxLength: 128, nullable: false), + UserKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + ExpiresAt = table.Column(type: "TEXT", nullable: false), + RevokedAt = table.Column(type: "TEXT", nullable: true), + SecurityVersionAtCreation = table.Column(type: "INTEGER", nullable: false), + Device = table.Column(type: "TEXT", nullable: false), + Claims = table.Column(type: "TEXT", nullable: false), + Metadata = table.Column(type: "TEXT", nullable: false), + Version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UAuth_Sessions", x => x.Id); + table.ForeignKey( + name: "FK_UAuth_Sessions_UAuth_SessionChains_Tenant_ChainId", + columns: x => new { x.Tenant, x.ChainId }, + principalTable: "UAuth_SessionChains", + principalColumns: new[] { "Tenant", "ChainId" }, + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Authentication_Tenant_LockedUntil", + table: "UAuth_Authentication", + columns: new[] { "Tenant", "LockedUntil" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Authentication_Tenant_ResetRequestedAt", + table: "UAuth_Authentication", + columns: new[] { "Tenant", "ResetRequestedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Authentication_Tenant_UserKey", + table: "UAuth_Authentication", + columns: new[] { "Tenant", "UserKey" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Authentication_Tenant_UserKey_Scope", + table: "UAuth_Authentication", + columns: new[] { "Tenant", "UserKey", "Scope" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Authentication_Tenant_UserKey_Scope_CredentialType", + table: "UAuth_Authentication", + columns: new[] { "Tenant", "UserKey", "Scope", "CredentialType" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_PasswordCredentials_Tenant_ExpiresAt", + table: "UAuth_PasswordCredentials", + columns: new[] { "Tenant", "ExpiresAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_PasswordCredentials_Tenant_Id", + table: "UAuth_PasswordCredentials", + columns: new[] { "Tenant", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_PasswordCredentials_Tenant_RevokedAt", + table: "UAuth_PasswordCredentials", + columns: new[] { "Tenant", "RevokedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_PasswordCredentials_Tenant_UserKey", + table: "UAuth_PasswordCredentials", + columns: new[] { "Tenant", "UserKey" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_PasswordCredentials_Tenant_UserKey_DeletedAt", + table: "UAuth_PasswordCredentials", + columns: new[] { "Tenant", "UserKey", "DeletedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_ChainId", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "ChainId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_ExpiresAt", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "ExpiresAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_ExpiresAt_RevokedAt", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "ExpiresAt", "RevokedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_ReplacedByTokenHash", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "ReplacedByTokenHash" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_SessionId", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "SessionId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_TokenHash", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "TokenHash" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_TokenHash_RevokedAt", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "TokenHash", "RevokedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_TokenId", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "TokenId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RefreshTokens_Tenant_UserKey", + table: "UAuth_RefreshTokens", + columns: new[] { "Tenant", "UserKey" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RolePermissions_Tenant_Permission", + table: "UAuth_RolePermissions", + columns: new[] { "Tenant", "Permission" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_RolePermissions_Tenant_RoleId", + table: "UAuth_RolePermissions", + columns: new[] { "Tenant", "RoleId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Roles_Tenant_Id", + table: "UAuth_Roles", + columns: new[] { "Tenant", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Roles_Tenant_NormalizedName", + table: "UAuth_Roles", + columns: new[] { "Tenant", "NormalizedName" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_SessionChains_Tenant_ChainId", + table: "UAuth_SessionChains", + columns: new[] { "Tenant", "ChainId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_SessionChains_Tenant_RootId", + table: "UAuth_SessionChains", + columns: new[] { "Tenant", "RootId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_SessionChains_Tenant_UserKey", + table: "UAuth_SessionChains", + columns: new[] { "Tenant", "UserKey" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_SessionChains_Tenant_UserKey_DeviceId", + table: "UAuth_SessionChains", + columns: new[] { "Tenant", "UserKey", "DeviceId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_SessionRoots_Tenant_RootId", + table: "UAuth_SessionRoots", + columns: new[] { "Tenant", "RootId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_SessionRoots_Tenant_UserKey", + table: "UAuth_SessionRoots", + columns: new[] { "Tenant", "UserKey" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Sessions_Tenant_ChainId", + table: "UAuth_Sessions", + columns: new[] { "Tenant", "ChainId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Sessions_Tenant_ChainId_RevokedAt", + table: "UAuth_Sessions", + columns: new[] { "Tenant", "ChainId", "RevokedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Sessions_Tenant_ExpiresAt", + table: "UAuth_Sessions", + columns: new[] { "Tenant", "ExpiresAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Sessions_Tenant_RevokedAt", + table: "UAuth_Sessions", + columns: new[] { "Tenant", "RevokedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Sessions_Tenant_SessionId", + table: "UAuth_Sessions", + columns: new[] { "Tenant", "SessionId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_Sessions_Tenant_UserKey_RevokedAt", + table: "UAuth_Sessions", + columns: new[] { "Tenant", "UserKey", "RevokedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserIdentifiers_Tenant_NormalizedValue", + table: "UAuth_UserIdentifiers", + columns: new[] { "Tenant", "NormalizedValue" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserIdentifiers_Tenant_Type_NormalizedValue", + table: "UAuth_UserIdentifiers", + columns: new[] { "Tenant", "Type", "NormalizedValue" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserIdentifiers_Tenant_UserKey", + table: "UAuth_UserIdentifiers", + columns: new[] { "Tenant", "UserKey" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserIdentifiers_Tenant_UserKey_IsPrimary", + table: "UAuth_UserIdentifiers", + columns: new[] { "Tenant", "UserKey", "IsPrimary" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserIdentifiers_Tenant_UserKey_Type_IsPrimary", + table: "UAuth_UserIdentifiers", + columns: new[] { "Tenant", "UserKey", "Type", "IsPrimary" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserLifecycles_Tenant_UserKey", + table: "UAuth_UserLifecycles", + columns: new[] { "Tenant", "UserKey" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserProfiles_Tenant_UserKey", + table: "UAuth_UserProfiles", + columns: new[] { "Tenant", "UserKey" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserRoles_Tenant_RoleId", + table: "UAuth_UserRoles", + columns: new[] { "Tenant", "RoleId" }); + + migrationBuilder.CreateIndex( + name: "IX_UAuth_UserRoles_Tenant_UserKey", + table: "UAuth_UserRoles", + columns: new[] { "Tenant", "UserKey" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UAuth_Authentication"); + + migrationBuilder.DropTable( + name: "UAuth_PasswordCredentials"); + + migrationBuilder.DropTable( + name: "UAuth_RefreshTokens"); + + migrationBuilder.DropTable( + name: "UAuth_RolePermissions"); + + migrationBuilder.DropTable( + name: "UAuth_Roles"); + + migrationBuilder.DropTable( + name: "UAuth_Sessions"); + + migrationBuilder.DropTable( + name: "UAuth_UserIdentifiers"); + + migrationBuilder.DropTable( + name: "UAuth_UserLifecycles"); + + migrationBuilder.DropTable( + name: "UAuth_UserProfiles"); + + migrationBuilder.DropTable( + name: "UAuth_UserRoles"); + + migrationBuilder.DropTable( + name: "UAuth_SessionChains"); + + migrationBuilder.DropTable( + name: "UAuth_SessionRoots"); + } + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs new file mode 100644 index 00000000..211ef12e --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs @@ -0,0 +1,707 @@ +// +using System; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations +{ + [DbContext(typeof(UAuthDbContext))] + partial class UAuthDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CredentialType") + .HasColumnType("INTEGER"); + + b.Property("FailedAttempts") + .HasColumnType("INTEGER"); + + b.Property("LastFailedAt") + .HasColumnType("TEXT"); + + b.Property("LockedUntil") + .HasColumnType("TEXT"); + + b.Property("RequiresReauthentication") + .HasColumnType("INTEGER"); + + b.Property("ResetAttempts") + .HasColumnType("INTEGER"); + + b.Property("ResetConsumedAt") + .HasColumnType("TEXT"); + + b.Property("ResetExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ResetRequestedAt") + .HasColumnType("TEXT"); + + b.Property("ResetTokenHash") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Scope") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "LockedUntil"); + + b.HasIndex("Tenant", "ResetRequestedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "Scope"); + + b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") + .IsUnique(); + + b.ToTable("UAuth_Authentication", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "RoleId", "Permission"); + + b.HasIndex("Tenant", "Permission"); + + b.HasIndex("Tenant", "RoleId"); + + b.ToTable("UAuth_RolePermissions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "NormalizedName") + .IsUnique(); + + b.ToTable("UAuth_Roles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("AssignedAt") + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "UserKey", "RoleId"); + + b.HasIndex("Tenant", "RoleId"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserRoles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeletedAt"); + + b.ToTable("UAuth_PasswordCredentials", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ActiveSessionId") + .HasColumnType("TEXT"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("ClaimsSnapshot") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasColumnType("TEXT"); + + b.Property("RotationCount") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TouchCount") + .HasColumnType("INTEGER"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId") + .IsUnique(); + + b.HasIndex("Tenant", "RootId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeviceId"); + + b.ToTable("UAuth_SessionChains", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "SessionId") + .IsUnique(); + + b.HasIndex("Tenant", "ChainId", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_Sessions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "RootId") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.ToTable("UAuth_SessionRoots", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenId") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "ReplacedByTokenHash"); + + b.HasIndex("Tenant", "SessionId"); + + b.HasIndex("Tenant", "TokenHash") + .IsUnique(); + + b.HasIndex("Tenant", "TokenId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); + + b.HasIndex("Tenant", "TokenHash", "RevokedAt"); + + b.ToTable("UAuth_RefreshTokens", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("NormalizedValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("VerifiedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "NormalizedValue"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "Type", "NormalizedValue") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey", "IsPrimary"); + + b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary"); + + b.ToTable("UAuth_UserIdentifiers", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.ToTable("UAuth_UserLifecycles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Bio") + .HasColumnType("TEXT"); + + b.Property("BirthDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Culture") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("Gender") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TimeZone") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserProfiles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) + .WithMany() + .HasForeignKey("Tenant", "RootId") + .HasPrincipalKey("Tenant", "RootId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) + .WithMany() + .HasForeignKey("Tenant", "ChainId") + .HasPrincipalKey("Tenant", "ChainId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs new file mode 100644 index 00000000..377eb8b1 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Program.cs @@ -0,0 +1,111 @@ +using CodeBeam.UltimateAuth.Client.Blazor; +using CodeBeam.UltimateAuth.Client.Blazor.Extensions; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.Infrastructure; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Components; +using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Infrastructure; +using CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore; +using CodeBeam.UltimateAuth.Sample.Seed.Extensions; +using CodeBeam.UltimateAuth.Server.Extensions; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.EntityFrameworkCore; +using MudBlazor.Services; +using MudExtensions.Services; +using Scalar.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +#region Core + +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents() + .AddCircuitOptions(options => + { + options.DetailedErrors = true; + }); + +builder.Services.AddOpenApi(); + +#endregion + +# region UI & MudBlazor & Extensions + +builder.Services.AddMudServices(o => { + o.SnackbarConfiguration.PreventDuplicates = false; +}); +builder.Services.AddMudExtensions(); +builder.Services.AddScoped(); + +#endregion + + +builder.Services.AddUltimateAuthServer(o => +{ + o.Diagnostics.EnableRefreshDetails = true; + //o.Session.MaxLifetime = TimeSpan.FromSeconds(32); + //o.Session.Lifetime = TimeSpan.FromSeconds(32); + //o.Session.TouchInterval = TimeSpan.FromSeconds(9); + //o.Session.IdleTimeout = TimeSpan.FromSeconds(15); + //o.Token.AccessTokenLifetime = TimeSpan.FromSeconds(30); + //o.Token.RefreshTokenLifetime = TimeSpan.FromSeconds(32); + o.Login.MaxFailedAttempts = 2; + o.Login.LockoutDuration = TimeSpan.FromSeconds(10); + o.Identifiers.AllowMultipleUsernames = true; +}) + .AddUltimateAuthEntityFrameworkCore(db => + { + db.UseSqlite("Data Source=uauth.db", x => x.MigrationsAssembly("CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore")); + }); + +builder.Services.AddUltimateAuthClientBlazor(o => +{ + //o.AutoRefresh.Interval = TimeSpan.FromSeconds(5); + o.Reauth.Behavior = ReauthBehavior.RaiseEvent; + //o.UAuthStateRefreshMode = UAuthStateRefreshMode.Validate; +}); + +builder.Services.AddScopedUltimateAuthSampleSeed(); + +builder.Services.Configure(options => +{ + options.ForwardedHeaders = + ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto; +}); + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + app.UseHsts(); +} +else +{ + app.MapOpenApi(); + app.MapScalarApiReference(); + + using (var scope = app.Services.CreateScope()) + { + await UAuthDbInitializer.InitializeAsync(app.Services, reset: true); + + var seedRunner = scope.ServiceProvider.GetRequiredService(); + await seedRunner.RunAsync(null); + } +} + +app.UseForwardedHeaders(); + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseUltimateAuthWithAspNetCore(); +app.UseAntiforgery(); + +app.MapUltimateAuthEndpoints(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddUltimateAuthRoutes(UAuthAssemblies.BlazorClient()); + +app.Run(); diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Properties/launchSettings.json b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Properties/launchSettings.json new file mode 100644 index 00000000..45dcb3a9 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5276", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7230;http://localhost:5276", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Seed/UAuthDbInitializer.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Seed/UAuthDbInitializer.cs new file mode 100644 index 00000000..4b4e23c4 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Seed/UAuthDbInitializer.cs @@ -0,0 +1,46 @@ +using CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; +using CodeBeam.UltimateAuth.Users.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore; + +public static class UAuthDbInitializer +{ + public static async Task InitializeAsync(IServiceProvider services, bool reset = false) + { + using var scope = services.CreateScope(); + var sp = scope.ServiceProvider; + + var bundleDb = sp.GetService(); + + if (bundleDb != null) + { + if (reset) + await bundleDb.Database.EnsureDeletedAsync(); + + await bundleDb.Database.MigrateAsync(); + return; + } + + var contexts = new DbContext[] + { + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService() + }; + + if (reset) + await contexts[0].Database.EnsureDeletedAsync(); + + foreach (var db in contexts) + await db.Database.MigrateAsync(); + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/appsettings.Development.json b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/appsettings.json b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db new file mode 100644 index 00000000..4e86411b Binary files /dev/null and b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db differ diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm new file mode 100644 index 00000000..9c25688e Binary files /dev/null and b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm differ diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal new file mode 100644 index 00000000..42498f65 Binary files /dev/null and b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal differ diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/wwwroot/UltimateAuth-Logo.png b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/wwwroot/UltimateAuth-Logo.png new file mode 100644 index 00000000..5b7282f1 Binary files /dev/null and b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/wwwroot/UltimateAuth-Logo.png differ diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/wwwroot/app.css b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/wwwroot/app.css new file mode 100644 index 00000000..2b9a4745 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/wwwroot/app.css @@ -0,0 +1,143 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +a, .btn-link { + color: #006bb7; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; +} + +.content { + padding-top: 1.1rem; +} + +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.darker-border-checkbox.form-check-input { + border-color: #929292; +} + +.uauth-stack { + min-height: 60vh; + max-height: calc(100vh - var(--mud-appbar-height)); + width: 30vw; + min-width: 300px; +} + +.uauth-menu-popover { + width: 300px; +} + +.uauth-login-paper { + min-height: 70vh; +} + +.uauth-login-paper.mud-theme-primary { + background: linear-gradient(145deg, var(--mud-palette-primary), rgba(0, 0, 0, 0.85) ); + color: white; +} + +.uauth-brand-glow { + filter: drop-shadow(0 0 25px rgba(255,255,255,0.15)); +} + +.uauth-logo-slide { + animation: uauth-logo-float 30s ease-in-out infinite; +} + +.uauth-text-transform-none .mud-button { + text-transform: none; +} + +.uauth-dialog { + height: 68vh; + max-height: 68vh; + overflow: auto; +} + +.text-secondary { + color: var(--mud-palette-text-secondary); +} + +.uauth-blur { + backdrop-filter: blur(10px); +} + +.uauth-blur-slight { + backdrop-filter: blur(4px); +} + +@keyframes uauth-logo-float { + 0% { + transform: translateY(0) rotateY(0); + } + + 10% { + transform: translateY(0) rotateY(0); + } + + 15% { + transform: translateY(200px) rotateY(360deg); + } + + 35% { + transform: translateY(200px) rotateY(360deg); + } + + 40% { + transform: translateY(200px) rotateY(720deg); + } + + 60% { + transform: translateY(200px) rotateY(720deg); + } + + 65% { + transform: translateY(0) rotateY(360deg); + } + + 85% { + transform: translateY(0) rotateY(360deg); + } + + 90% { + transform: translateY(0) rotateY(0); + } + + 100% { + transform: translateY(0) rotateY(0); + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.slnx b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.slnx deleted file mode 100644 index 27a855de..00000000 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.slnx +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj index c27fc66a..9d71e7d7 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/CodeBeam.UltimateAuth.Sample.BlazorServer.csproj @@ -17,7 +17,7 @@ - + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor index e32cc79c..881cae5c 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/Register.razor @@ -45,6 +45,12 @@ Sign Up + + + + + Already have an account? SignIn + diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs index 5995bf9f..7b54009b 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs @@ -1,15 +1,17 @@ +using CodeBeam.UltimateAuth.Client.Blazor; using CodeBeam.UltimateAuth.Client.Blazor.Extensions; +using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.Infrastructure; using CodeBeam.UltimateAuth.InMemory; using CodeBeam.UltimateAuth.Sample.BlazorServer.Components; using CodeBeam.UltimateAuth.Sample.BlazorServer.Infrastructure; +using CodeBeam.UltimateAuth.Sample.Seed.Extensions; using CodeBeam.UltimateAuth.Server.Extensions; using Microsoft.AspNetCore.HttpOverrides; using MudBlazor.Services; using MudExtensions.Services; using Scalar.AspNetCore; -using CodeBeam.UltimateAuth.Client.Blazor; var builder = WebApplication.CreateBuilder(args); @@ -59,6 +61,7 @@ //o.UAuthStateRefreshMode = UAuthStateRefreshMode.Validate; }); +builder.Services.AddUltimateAuthSampleSeed(); builder.Services.Configure(options => { @@ -67,7 +70,6 @@ ForwardedHeaders.XForwardedProto; }); - var app = builder.Build(); if (!app.Environment.IsDevelopment()) diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Register.razor b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Register.razor index e32cc79c..881cae5c 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Register.razor +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Pages/Register.razor @@ -45,6 +45,12 @@ Sign Up + + + + + Already have an account? SignIn + diff --git a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs index 7bc8536b..6e346489 100644 --- a/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs +++ b/samples/blazor-standalone-wasm/CodeBeam.UltimateAuth.Sample.BlazorStandaloneWasm/Program.cs @@ -13,36 +13,31 @@ builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); +builder.Services.AddMudServices(o => { + o.SnackbarConfiguration.PreventDuplicates = false; +}); +builder.Services.AddMudExtensions(); +builder.Services.AddScoped(); + + builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddUltimateAuth(); builder.Services.AddUltimateAuthClientBlazor(o => { - o.Endpoints.BasePath = "https://localhost:6110/auth"; + o.Endpoints.BasePath = "https://localhost:6110/auth"; // UAuthHub URL o.Reauth.Behavior = ReauthBehavior.RaiseEvent; o.Login.AllowCredentialPost = true; - o.Pkce.ReturnUrl = "https://localhost:6130/home"; + o.Pkce.ReturnUrl = "https://localhost:6130/home"; // This application domain + path }); -builder.Services.AddMudServices(o => { - o.SnackbarConfiguration.PreventDuplicates = false; -}); -builder.Services.AddMudExtensions(); - builder.Services.AddScoped(); -builder.Services.AddScoped(); - - -//builder.Services.AddHttpClient("ResourceApi", client => -//{ -// client.BaseAddress = new Uri("https://localhost:6120"); -//}); builder.Services.AddScoped(sp => { return new HttpClient { - BaseAddress = new Uri("https://localhost:6120") // Resource API + BaseAddress = new Uri("https://localhost:6120") // Resource API URL }; }); diff --git a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs index 35eb05d1..a59b1f61 100644 --- a/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs +++ b/samples/resource-api/CodeBeam.UltimateAuth.Sample.ResourceApi/Program.cs @@ -17,13 +17,9 @@ { app.MapOpenApi(); } - app.UseHttpsRedirection(); -app.UseUltimateAuthResourceApi(); -app.UseAuthentication(); -app.UseAuthorization(); +app.UseUltimateAuthResourceApiWithAspNetCore(); app.MapControllers(); - app.Run(); diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/EndpointRouteBuilderExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/EndpointRouteBuilderExtensions.cs index cedcbdfe..bf1bf3cb 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/EndpointRouteBuilderExtensions.cs @@ -1,4 +1,5 @@ -using CodeBeam.UltimateAuth.Server.Endpoints; +using CodeBeam.UltimateAuth.Core.Runtime; +using CodeBeam.UltimateAuth.Server.Endpoints; using CodeBeam.UltimateAuth.Server.Options; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; @@ -11,10 +12,21 @@ public static class EndpointRouteBuilderExtensions { public static IEndpointRouteBuilder MapUltimateAuthEndpoints(this IEndpointRouteBuilder endpoints) { - var registrar = endpoints.ServiceProvider.GetRequiredService(); - var options = endpoints.ServiceProvider.GetRequiredService>().Value; - var rootGroup = endpoints.MapGroup("") - .RequireCors("UAuthHub"); + var sp = endpoints.ServiceProvider; + + var registrar = sp.GetRequiredService(); + var options = sp.GetRequiredService>().Value; + + var marker = sp.GetService(); + var requiresCors = marker?.RequiresCors == true; + + var rootGroup = endpoints.MapGroup(""); + + if (requiresCors) + { + rootGroup = rootGroup.RequireCors("UAuthHub"); + } + registrar.MapEndpoints(rootGroup, options); if (endpoints is WebApplication app) diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs index 6c6977dd..a9da44d6 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs @@ -256,8 +256,6 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol services.TryAddScoped(); services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddScoped(); // Endpoints diff --git a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs index c78d6427..d352d7f1 100644 --- a/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs +++ b/src/CodeBeam.UltimateAuth.Server/Extensions/UAuthApplicationBuilderExtensions.cs @@ -65,4 +65,13 @@ public static IApplicationBuilder UseUltimateAuthResourceApi(this IApplicationBu return app; } + + public static IApplicationBuilder UseUltimateAuthResourceApiWithAspNetCore(this IApplicationBuilder app) + { + app.UseUltimateAuthResourceApi(); + app.UseAuthentication(); + app.UseAuthorization(); + + return app; + } } diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthSessionIssuer.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthSessionIssuer.cs index 4f1d4909..66d5b8ee 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthSessionIssuer.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Issuers/UAuthSessionIssuer.cs @@ -67,8 +67,30 @@ await kernel.ExecuteAsync(async _ => if (context.ChainId is not null) { - chain = await kernel.GetChainAsync(context.ChainId.Value) - ?? throw new UAuthNotFoundException("Chain not found."); + var existing = await kernel.GetChainAsync(context.ChainId.Value); + + if (existing is null) + { + chain = UAuthSessionChain.Create( + SessionChainId.New(), + root.RootId, + context.Tenant, + context.UserKey, + now, + expiresAt, + context.Device, + ClaimsSnapshot.Empty, + root.SecurityVersion + ); + await kernel.CreateChainAsync(chain); + } + else + { + chain = existing; + } + + //chain = await kernel.GetChainAsync(context.ChainId.Value) + // ?? throw new UAuthNotFoundException("Chain not found."); if (chain.IsRevoked) throw new UAuthValidationException("Chain revoked."); diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Data/UAuthAuthenticationDbContext.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Data/UAuthAuthenticationDbContext.cs index c2ff82b3..c7c19cc1 100644 --- a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Data/UAuthAuthenticationDbContext.cs +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Data/UAuthAuthenticationDbContext.cs @@ -1,10 +1,8 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; -internal sealed class UAuthAuthenticationDbContext : DbContext +public sealed class UAuthAuthenticationDbContext : DbContext { public DbSet AuthenticationSecurityStates => Set(); @@ -14,65 +12,8 @@ public UAuthAuthenticationDbContext(DbContextOptions(e => - { - e.ToTable("UAuth_Authentication"); - e.HasKey(x => x.Id); - - e.Property(x => x.SecurityVersion).IsConcurrencyToken(); - - e.Property(x => x.Tenant) - .HasConversion( - v => v.Value, - v => TenantKey.FromInternal(v)) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.UserKey) - .HasConversion( - v => v.Value, - v => UserKey.FromString(v)) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.Scope) - .IsRequired(); - - e.Property(x => x.CredentialType); - - e.Property(x => x.FailedAttempts) - .IsRequired(); - - e.Property(x => x.LastFailedAt); - - e.Property(x => x.LockedUntil); - - e.Property(x => x.RequiresReauthentication) - .IsRequired(); - - e.Property(x => x.ResetRequestedAt); - e.Property(x => x.ResetExpiresAt); - e.Property(x => x.ResetConsumedAt); - - e.Property(x => x.ResetTokenHash) - .HasMaxLength(512); - - e.Property(x => x.ResetAttempts) - .IsRequired(); - - e.HasIndex(x => new { x.Tenant, x.UserKey, x.Scope, x.CredentialType }).IsUnique(); - e.HasIndex(x => new { x.Tenant, x.UserKey }); - e.HasIndex(x => new { x.Tenant, x.LockedUntil }); - e.HasIndex(x => new { x.Tenant, x.ResetRequestedAt }); - e.HasIndex(x => new { x.Tenant, x.UserKey, x.Scope }); - - }); + UAuthAuthenticationModelBuilder.Configure(modelBuilder); } } diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Data/UAuthAuthenticationModelBuilder.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Data/UAuthAuthenticationModelBuilder.cs new file mode 100644 index 00000000..4689db06 --- /dev/null +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Data/UAuthAuthenticationModelBuilder.cs @@ -0,0 +1,75 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; + +public static class UAuthAuthenticationModelBuilder +{ + public static void Configure(ModelBuilder b) + { + ConfigureAuthenticationSecurityState(b); + } + + private static void ConfigureAuthenticationSecurityState(ModelBuilder b) + { + b.Entity(e => + { + e.ToTable("UAuth_Authentication"); + + e.HasKey(x => x.Id); + + e.Property(x => x.SecurityVersion) + .IsConcurrencyToken(); + + e.Property(x => x.Tenant) + .HasConversion(v => v.Value, v => TenantKey.FromInternal(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.UserKey) + .HasConversion(v => v.Value, v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.Scope) + .IsRequired(); + + e.Property(x => x.CredentialType); + + e.Property(x => x.FailedAttempts) + .IsRequired(); + + e.Property(x => x.LastFailedAt) + .HasNullableUtcDateTimeOffsetConverter(); + + e.Property(x => x.LockedUntil) + .HasNullableUtcDateTimeOffsetConverter(); + + e.Property(x => x.RequiresReauthentication) + .IsRequired(); + + e.Property(x => x.ResetRequestedAt) + .HasNullableUtcDateTimeOffsetConverter(); + + e.Property(x => x.ResetExpiresAt) + .HasNullableUtcDateTimeOffsetConverter(); + + e.Property(x => x.ResetConsumedAt) + .HasNullableUtcDateTimeOffsetConverter(); + + e.Property(x => x.ResetTokenHash) + .HasMaxLength(512); + + e.Property(x => x.ResetAttempts) + .IsRequired(); + + e.HasIndex(x => new { x.Tenant, x.UserKey, x.Scope, x.CredentialType }).IsUnique(); + e.HasIndex(x => new { x.Tenant, x.UserKey }); + e.HasIndex(x => new { x.Tenant, x.LockedUntil }); + e.HasIndex(x => new { x.Tenant, x.ResetRequestedAt }); + e.HasIndex(x => new { x.Tenant, x.UserKey, x.Scope }); + }); + } +} \ No newline at end of file diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs index 956212fa..1a9db677 100644 --- a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -6,10 +6,14 @@ namespace CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthAuthenticationEntityFrameworkCore(this IServiceCollection services, Action configureDb) + public static IServiceCollection AddUltimateAuthAuthenticationEntityFrameworkCore(this IServiceCollection services, Action? configureDb = null) where TDbContext : DbContext { - services.AddDbContext(configureDb); - services.AddScoped(); + if (configureDb != null) + { + services.AddDbContext(configureDb); + } + + services.AddScoped>(); return services; } } diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Projections/AuthenticationSecutiryStateProjection.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Projections/AuthenticationSecutiryStateProjection.cs index fc9740ad..266bf15a 100644 --- a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Projections/AuthenticationSecutiryStateProjection.cs +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Projections/AuthenticationSecutiryStateProjection.cs @@ -3,7 +3,7 @@ namespace CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; -internal sealed class AuthenticationSecurityStateProjection +public sealed class AuthenticationSecurityStateProjection { public Guid Id { get; set; } diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs index ac247165..fe12bfcd 100644 --- a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStore.cs @@ -7,20 +7,22 @@ namespace CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; -internal sealed class EfCoreAuthenticationSecurityStateStore : IAuthenticationSecurityStateStore +internal sealed class EfCoreAuthenticationSecurityStateStore : IAuthenticationSecurityStateStore where TDbContext : DbContext { - private readonly UAuthAuthenticationDbContext _db; + private readonly TDbContext _db; private readonly TenantKey _tenant; - public EfCoreAuthenticationSecurityStateStore(UAuthAuthenticationDbContext db, TenantContext tenant) + public EfCoreAuthenticationSecurityStateStore(TDbContext db, TenantContext tenant) { _db = db; _tenant = tenant.Tenant; } + private DbSet DbSet => _db.Set(); + public async Task GetAsync(UserKey userKey, AuthenticationSecurityScope scope, CredentialType? credentialType, CancellationToken ct = default) { - var entity = await _db.AuthenticationSecurityStates + var entity = await DbSet .AsNoTracking() .SingleOrDefaultAsync(x => x.Tenant == _tenant && @@ -38,14 +40,14 @@ public async Task AddAsync(AuthenticationSecurityState state, CancellationToken { var entity = AuthenticationSecurityStateMapper.ToProjection(state); - _db.AuthenticationSecurityStates.Add(entity); + DbSet.Add(entity); await _db.SaveChangesAsync(ct); } public async Task UpdateAsync(AuthenticationSecurityState state, long expectedVersion, CancellationToken ct = default) { - var entity = await _db.AuthenticationSecurityStates + var entity = await DbSet .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.Id == state.Id, @@ -64,7 +66,7 @@ public async Task UpdateAsync(AuthenticationSecurityState state, long expectedVe public async Task DeleteAsync(UserKey userKey, AuthenticationSecurityScope scope, CredentialType? credentialType, CancellationToken ct = default) { - var entity = await _db.AuthenticationSecurityStates + var entity = await DbSet .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == userKey && @@ -75,7 +77,7 @@ public async Task DeleteAsync(UserKey userKey, AuthenticationSecurityScope scope if (entity is null) return; - _db.AuthenticationSecurityStates.Remove(entity); + DbSet.Remove(entity); await _db.SaveChangesAsync(ct); } diff --git a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStoreFactory.cs b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStoreFactory.cs index 74ab7382..5f897cf7 100644 --- a/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStoreFactory.cs +++ b/src/authentication/CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore/Stores/EfCoreAuthenticationSecurityStateStoreFactory.cs @@ -1,19 +1,20 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore; -internal sealed class EfCoreAuthenticationSecurityStateStoreFactory : IAuthenticationSecurityStateStoreFactory +internal sealed class EfCoreAuthenticationSecurityStateStoreFactory : IAuthenticationSecurityStateStoreFactory where TDbContext : DbContext { - private readonly UAuthAuthenticationDbContext _db; + private readonly TDbContext _db; - public EfCoreAuthenticationSecurityStateStoreFactory(UAuthAuthenticationDbContext db) + public EfCoreAuthenticationSecurityStateStoreFactory(TDbContext db) { _db = db; } public IAuthenticationSecurityStateStore Create(TenantKey tenant) { - return new EfCoreAuthenticationSecurityStateStore(_db, new TenantContext(tenant)); + return new EfCoreAuthenticationSecurityStateStore(_db, new TenantContext(tenant)); } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Data/UAuthAuthorizationDbContext.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Data/UAuthAuthorizationDbContext.cs index 7783f457..187e1d4d 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Data/UAuthAuthorizationDbContext.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Data/UAuthAuthorizationDbContext.cs @@ -1,11 +1,8 @@ -using CodeBeam.UltimateAuth.Authorization.Contracts; -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; -internal sealed class UAuthAuthorizationDbContext : DbContext +public sealed class UAuthAuthorizationDbContext : DbContext { public DbSet Roles => Set(); public DbSet RolePermissions => Set(); @@ -16,115 +13,8 @@ public UAuthAuthorizationDbContext(DbContextOptions { } - protected override void OnModelCreating(ModelBuilder b) + protected override void OnModelCreating(ModelBuilder modelBuilder) { - ConfigureRole(b); - ConfigureRolePermission(b); - ConfigureUserRole(b); + UAuthAuthorizationModelBuilder.Configure(modelBuilder); } - - private void ConfigureRole(ModelBuilder b) - { - b.Entity(e => - { - e.ToTable("UAuth_Roles"); - e.HasKey(x => x.Id); - - e.Property(x => x.Version) - .IsConcurrencyToken(); - - e.Property(x => x.Tenant) - .HasConversion( - v => v.Value, - v => TenantKey.FromInternal(v)) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.Id) - .HasConversion( - v => v.Value, - v => RoleId.From(v)) - .IsRequired(); - - e.Property(x => x.Name) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.NormalizedName) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.CreatedAt) - .HasConversion( - v => v.UtcDateTime, - v => new DateTimeOffset(v, TimeSpan.Zero)); - - e.HasIndex(x => new { x.Tenant, x.Id }).IsUnique(); - e.HasIndex(x => new { x.Tenant, x.NormalizedName }).IsUnique(); - }); - } - - private void ConfigureRolePermission(ModelBuilder b) - { - b.Entity(e => - { - e.ToTable("UAuth_RolePermissions"); - e.HasKey(x => new { x.Tenant, x.RoleId, x.Permission }); - - e.Property(x => x.Tenant) - .HasConversion( - v => v.Value, - v => TenantKey.FromInternal(v)) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.RoleId) - .HasConversion( - v => v.Value, - v => RoleId.From(v)) - .IsRequired(); - - e.Property(x => x.Permission) - .HasMaxLength(256) - .IsRequired(); - - e.HasIndex(x => new { x.Tenant, x.RoleId }); - e.HasIndex(x => new { x.Tenant, x.Permission }); - }); - } - - private void ConfigureUserRole(ModelBuilder b) - { - b.Entity(e => - { - e.ToTable("UAuth_UserRoles"); - e.HasKey(x => new { x.Tenant, x.UserKey, x.RoleId }); - - e.Property(x => x.Tenant) - .HasConversion( - v => v.Value, - v => TenantKey.FromInternal(v)) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.UserKey) - .HasConversion( - v => v.Value, - v => UserKey.FromString(v)) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.RoleId) - .HasConversion( - v => v.Value, - v => RoleId.From(v)) - .IsRequired(); - - e.Property(x => x.AssignedAt) - .IsRequired(); - - e.HasIndex(x => new { x.Tenant, x.UserKey }); - e.HasIndex(x => new { x.Tenant, x.RoleId }); - }); - } -} \ No newline at end of file +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Data/UAuthAuthorizationModelBuilder.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Data/UAuthAuthorizationModelBuilder.cs new file mode 100644 index 00000000..1d50fdbb --- /dev/null +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Data/UAuthAuthorizationModelBuilder.cs @@ -0,0 +1,109 @@ +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; + +public static class UAuthAuthorizationModelBuilder +{ + public static void Configure(ModelBuilder b) + { + ConfigureRoles(b); + ConfigureRolePermissions(b); + ConfigureUserRoles(b); + } + + private static void ConfigureRoles(ModelBuilder b) + { + b.Entity(e => + { + e.ToTable("UAuth_Roles"); + + e.HasKey(x => x.Id); + + e.Property(x => x.Version) + .IsConcurrencyToken(); + + e.Property(x => x.Tenant) + .HasConversion(v => v.Value, v => TenantKey.FromInternal(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.Id) + .HasConversion(v => v.Value, v => RoleId.From(v)) + .IsRequired(); + + e.Property(x => x.Name) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.NormalizedName) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.CreatedAt).HasUtcDateTimeOffsetConverter().IsRequired(); + e.Property(x => x.UpdatedAt).HasNullableUtcDateTimeOffsetConverter(); + e.Property(x => x.DeletedAt).HasNullableUtcDateTimeOffsetConverter(); + + e.HasIndex(x => new { x.Tenant, x.Id }).IsUnique(); + e.HasIndex(x => new { x.Tenant, x.NormalizedName }).IsUnique(); + }); + } + + private static void ConfigureRolePermissions(ModelBuilder b) + { + b.Entity(e => + { + e.ToTable("UAuth_RolePermissions"); + + e.HasKey(x => new { x.Tenant, x.RoleId, x.Permission }); + + e.Property(x => x.Tenant) + .HasConversion(v => v.Value, v => TenantKey.FromInternal(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.RoleId) + .HasConversion(v => v.Value, v => RoleId.From(v)) + .IsRequired(); + + e.Property(x => x.Permission) + .HasMaxLength(256) + .IsRequired(); + + e.HasIndex(x => new { x.Tenant, x.RoleId }); + e.HasIndex(x => new { x.Tenant, x.Permission }); + }); + } + + private static void ConfigureUserRoles(ModelBuilder b) + { + b.Entity(e => + { + e.ToTable("UAuth_UserRoles"); + + e.HasKey(x => new { x.Tenant, x.UserKey, x.RoleId }); + + e.Property(x => x.Tenant) + .HasConversion(v => v.Value, v => TenantKey.FromInternal(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.UserKey) + .HasConversion(v => v.Value, v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.RoleId) + .HasConversion(v => v.Value, v => RoleId.From(v)) + .IsRequired(); + + e.Property(x => x.AssignedAt).HasUtcDateTimeOffsetConverter().IsRequired(); + + e.HasIndex(x => new { x.Tenant, x.UserKey }); + e.HasIndex(x => new { x.Tenant, x.RoleId }); + }); + } +} diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs index b4554cd4..8ed556c8 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -5,11 +5,15 @@ namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthAuthorizationEntityFrameworkCore(this IServiceCollection services, Action configureDb) + public static IServiceCollection AddUltimateAuthAuthorizationEntityFrameworkCore(this IServiceCollection services, Action? configureDb = null) where TDbContext : DbContext { - services.AddDbContext(configureDb); - services.AddScoped(); - services.AddScoped(); + if (configureDb != null) + { + services.AddDbContext(configureDb); + } + + services.AddScoped>(); + services.AddScoped>(); return services; } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Projections/RolePermissionProjection.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Projections/RolePermissionProjection.cs index 571425a7..bc12b9a0 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Projections/RolePermissionProjection.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Projections/RolePermissionProjection.cs @@ -3,7 +3,7 @@ namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; -internal sealed class RolePermissionProjection +public sealed class RolePermissionProjection { public TenantKey Tenant { get; set; } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Projections/RoleProjection.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Projections/RoleProjection.cs index 1c4e155a..e4e5e80e 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Projections/RoleProjection.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Projections/RoleProjection.cs @@ -3,7 +3,7 @@ namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; -internal sealed class RoleProjection +public sealed class RoleProjection { public RoleId Id { get; set; } @@ -14,9 +14,7 @@ internal sealed class RoleProjection public string NormalizedName { get; set; } = default!; public DateTimeOffset CreatedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } - public DateTimeOffset? DeletedAt { get; set; } public long Version { get; set; } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Projections/UserRoleProjection.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Projections/UserRoleProjection.cs index 42f1f186..22f956e2 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Projections/UserRoleProjection.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Projections/UserRoleProjection.cs @@ -4,7 +4,7 @@ namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; -internal sealed class UserRoleProjection +public sealed class UserRoleProjection { public TenantKey Tenant { get; set; } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs index 8c58de33..f5151afa 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStore.cs @@ -6,20 +6,23 @@ namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; -internal sealed class EfCoreRoleStore : IRoleStore +internal sealed class EfCoreRoleStore : IRoleStore where TDbContext : DbContext { - private readonly UAuthAuthorizationDbContext _db; + private readonly TDbContext _db; private readonly TenantKey _tenant; - public EfCoreRoleStore(UAuthAuthorizationDbContext db, TenantContext tenant) + public EfCoreRoleStore(TDbContext db, TenantContext tenant) { _db = db; _tenant = tenant.Tenant; } + private DbSet DbSetRole => _db.Set(); + private DbSet DbSetPermission => _db.Set(); + public async Task ExistsAsync(RoleKey key, CancellationToken ct = default) { - return await _db.Roles + return await DbSetRole .AnyAsync(x => x.Tenant == _tenant && x.Id == key.RoleId, @@ -28,7 +31,7 @@ public async Task ExistsAsync(RoleKey key, CancellationToken ct = default) public async Task AddAsync(Role role, CancellationToken ct = default) { - var exists = await _db.Roles + var exists = await DbSetRole .AnyAsync(x => x.Tenant == _tenant && x.NormalizedName == role.NormalizedName && @@ -40,19 +43,19 @@ public async Task AddAsync(Role role, CancellationToken ct = default) var entity = RoleMapper.ToProjection(role); - _db.Roles.Add(entity); + DbSetRole.Add(entity); var permissionEntities = role.Permissions .Select(p => RolePermissionMapper.ToProjection(_tenant, role.Id, p)); - _db.RolePermissions.AddRange(permissionEntities); + DbSetPermission.AddRange(permissionEntities); await _db.SaveChangesAsync(ct); } public async Task GetAsync(RoleKey key, CancellationToken ct = default) { - var entity = await _db.Roles + var entity = await DbSetRole .AsNoTracking() .SingleOrDefaultAsync(x => x.Tenant == _tenant && @@ -62,7 +65,7 @@ public async Task AddAsync(Role role, CancellationToken ct = default) if (entity is null) return null; - var permissions = await _db.RolePermissions + var permissions = await DbSetPermission .AsNoTracking() .Where(x => x.Tenant == _tenant && @@ -74,7 +77,7 @@ public async Task AddAsync(Role role, CancellationToken ct = default) public async Task SaveAsync(Role role, long expectedVersion, CancellationToken ct = default) { - var entity = await _db.Roles + var entity = await DbSetRole .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.Id == role.Id, @@ -88,7 +91,7 @@ public async Task SaveAsync(Role role, long expectedVersion, CancellationToken c if (entity.NormalizedName != role.NormalizedName) { - var exists = await _db.Roles + var exists = await DbSetRole .AnyAsync(x => x.Tenant == _tenant && x.NormalizedName == role.NormalizedName && @@ -103,25 +106,25 @@ public async Task SaveAsync(Role role, long expectedVersion, CancellationToken c RoleMapper.UpdateProjection(role, entity); entity.Version++; - var existingPermissions = await _db.RolePermissions + var existingPermissions = await DbSetPermission .Where(x => x.Tenant == _tenant && x.RoleId == role.Id) .ToListAsync(ct); - _db.RolePermissions.RemoveRange(existingPermissions); + DbSetPermission.RemoveRange(existingPermissions); var newPermissions = role.Permissions .Select(p => RolePermissionMapper.ToProjection(_tenant, role.Id, p)); - _db.RolePermissions.AddRange(newPermissions); + DbSetPermission.AddRange(newPermissions); await _db.SaveChangesAsync(ct); } public async Task DeleteAsync(RoleKey key, long expectedVersion, DeleteMode mode, DateTimeOffset now, CancellationToken ct = default) { - var entity = await _db.Roles + var entity = await DbSetRole .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.Id == key.RoleId, @@ -135,13 +138,13 @@ public async Task DeleteAsync(RoleKey key, long expectedVersion, DeleteMode mode if (mode == DeleteMode.Hard) { - await _db.RolePermissions + await DbSetPermission .Where(x => x.Tenant == _tenant && x.RoleId == key.RoleId) .ExecuteDeleteAsync(ct); - _db.Roles.Remove(entity); + DbSetRole.Remove(entity); } else { @@ -154,7 +157,7 @@ await _db.RolePermissions public async Task GetByNameAsync(string normalizedName, CancellationToken ct = default) { - var entity = await _db.Roles + var entity = await DbSetRole .AsNoTracking() .SingleOrDefaultAsync(x => x.Tenant == _tenant && @@ -165,7 +168,7 @@ await _db.RolePermissions if (entity is null) return null; - var permissions = await _db.RolePermissions + var permissions = await DbSetPermission .AsNoTracking() .Where(x => x.Tenant == _tenant && @@ -179,7 +182,7 @@ public async Task> GetByIdsAsync( IReadOnlyCollection roleIds, CancellationToken ct = default) { - var entities = await _db.Roles + var entities = await DbSetRole .AsNoTracking() .Where(x => x.Tenant == _tenant && @@ -188,7 +191,7 @@ public async Task> GetByIdsAsync( var roleIdsSet = entities.Select(x => x.Id).ToList(); - var permissions = await _db.RolePermissions + var permissions = await DbSetPermission .AsNoTracking() .Where(x => x.Tenant == _tenant && @@ -217,7 +220,7 @@ public async Task> QueryAsync( { var normalized = query.Normalize(); - var baseQuery = _db.Roles + var baseQuery = DbSetRole .AsNoTracking() .Where(x => x.Tenant == _tenant); diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStoreFactory.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStoreFactory.cs index 975dc443..ed02cd5e 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStoreFactory.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreRoleStoreFactory.cs @@ -1,18 +1,19 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; +using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; -internal sealed class EfCoreRoleStoreFactory : IRoleStoreFactory +internal sealed class EfCoreRoleStoreFactory : IRoleStoreFactory where TDbContext : DbContext { - private readonly UAuthAuthorizationDbContext _db; + private readonly TDbContext _db; - public EfCoreRoleStoreFactory(UAuthAuthorizationDbContext db) + public EfCoreRoleStoreFactory(TDbContext db) { _db = db; } public IRoleStore Create(TenantKey tenant) { - return new EfCoreRoleStore(_db, new TenantContext(tenant)); + return new EfCoreRoleStore(_db, new TenantContext(tenant)); } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStore.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStore.cs index 24353a98..a99234f0 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStore.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStore.cs @@ -6,20 +6,22 @@ namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; -internal sealed class EfCoreUserRoleStore : IUserRoleStore +internal sealed class EfCoreUserRoleStore : IUserRoleStore where TDbContext : DbContext { - private readonly UAuthAuthorizationDbContext _db; + private readonly TDbContext _db; private readonly TenantKey _tenant; - public EfCoreUserRoleStore(UAuthAuthorizationDbContext db, TenantContext tenant) + public EfCoreUserRoleStore(TDbContext db, TenantContext tenant) { _db = db; _tenant = tenant.Tenant; } + private DbSet DbSet => _db.Set(); + public async Task> GetAssignmentsAsync(UserKey userKey, CancellationToken ct = default) { - var entities = await _db.UserRoles + var entities = await DbSet .AsNoTracking() .Where(x => x.Tenant == _tenant && @@ -31,7 +33,7 @@ public async Task> GetAssignmentsAsync(UserKey use public async Task AssignAsync(UserKey userKey, RoleId roleId, DateTimeOffset assignedAt, CancellationToken ct = default) { - var exists = await _db.UserRoles + var exists = await DbSet .AnyAsync(x => x.Tenant == _tenant && x.UserKey == userKey && @@ -49,13 +51,13 @@ public async Task AssignAsync(UserKey userKey, RoleId roleId, DateTimeOffset ass AssignedAt = assignedAt }; - _db.UserRoles.Add(entity); + DbSet.Add(entity); await _db.SaveChangesAsync(ct); } public async Task RemoveAsync(UserKey userKey, RoleId roleId, CancellationToken ct = default) { - var entity = await _db.UserRoles + var entity = await DbSet .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == userKey && @@ -65,13 +67,13 @@ public async Task RemoveAsync(UserKey userKey, RoleId roleId, CancellationToken if (entity is null) return; - _db.UserRoles.Remove(entity); + DbSet.Remove(entity); await _db.SaveChangesAsync(ct); } public async Task RemoveAssignmentsByRoleAsync(RoleId roleId, CancellationToken ct = default) { - await _db.UserRoles + await DbSet .Where(x => x.Tenant == _tenant && x.RoleId == roleId) @@ -80,7 +82,7 @@ await _db.UserRoles public async Task CountAssignmentsAsync(RoleId roleId, CancellationToken ct = default) { - return await _db.UserRoles + return await DbSet .CountAsync(x => x.Tenant == _tenant && x.RoleId == roleId, diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStoreFactory.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStoreFactory.cs index 516b5e9b..74132289 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStoreFactory.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore/Stores/EfCoreUserRoleStoreFactory.cs @@ -1,18 +1,19 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; +using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore; -internal sealed class EfCoreUserRoleStoreFactory : IUserRoleStoreFactory +internal sealed class EfCoreUserRoleStoreFactory : IUserRoleStoreFactory where TDbContext : DbContext { - private readonly UAuthAuthorizationDbContext _db; + private readonly TDbContext _db; - public EfCoreUserRoleStoreFactory(UAuthAuthorizationDbContext db) + public EfCoreUserRoleStoreFactory(TDbContext db) { _db = db; } public IUserRoleStore Create(TenantKey tenant) { - return new EfCoreUserRoleStore(_db, new TenantContext(tenant)); + return new EfCoreUserRoleStore(_db, new TenantContext(tenant)); } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs index 3ce68afc..919b6a61 100644 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs +++ b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/Extensions/ServiceCollectionExtensions.cs @@ -11,9 +11,6 @@ public static IServiceCollection AddUltimateAuthAuthorizationInMemory(this IServ services.TryAddSingleton(); services.TryAddSingleton(); - // Never try add - seeding is enumerated and all contributors are added. - services.AddSingleton(); - return services; } } diff --git a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/IAuthorizationSeeder.cs b/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/IAuthorizationSeeder.cs deleted file mode 100644 index 750a4d66..00000000 --- a/src/authorization/CodeBeam.UltimateAuth.Authorization.InMemory/IAuthorizationSeeder.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CodeBeam.UltimateAuth.Authorization.InMemory; - -public interface IAuthorizationSeeder -{ - Task SeedAsync(CancellationToken ct = default); -} diff --git a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs index 062de526..849e6fe0 100644 --- a/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs +++ b/src/client/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs @@ -23,17 +23,15 @@ internal class UAuthFlowClient : IFlowClient private readonly IUAuthRequestClient _post; private readonly IUAuthClientEvents _events; private readonly IClientDeviceProvider _clientDeviceProvider; - private readonly IDeviceIdProvider _deviceIdProvider; private readonly IReturnUrlProvider _returnUrlProvider; private readonly UAuthClientOptions _options; private readonly UAuthClientDiagnostics _diagnostics; - public UAuthFlowClient(IUAuthRequestClient post, IUAuthClientEvents events, IClientDeviceProvider clientDeviceProvider, IDeviceIdProvider deviceIdProvider, IReturnUrlProvider returnUrlProvider, IOptions options, UAuthClientDiagnostics diagnostics) + public UAuthFlowClient(IUAuthRequestClient post, IUAuthClientEvents events, IClientDeviceProvider clientDeviceProvider, IReturnUrlProvider returnUrlProvider, IOptions options, UAuthClientDiagnostics diagnostics) { _post = post; _events = events; _clientDeviceProvider = clientDeviceProvider; - _deviceIdProvider = deviceIdProvider; _returnUrlProvider = returnUrlProvider; _options = options.Value; _diagnostics = diagnostics; @@ -64,13 +62,22 @@ public async Task TryLoginAsync(LoginRequest request, UAuthSubmi { case UAuthSubmitMode.TryOnly: { - var result = await _post.SendJsonAsync(tryUrl, request); + var result = await _post.SendJsonAsync(tryUrl, payload); if (result.Body is null) throw new UAuthProtocolException("Empty response body."); - var parsed = result.Body.Value.Deserialize( - new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + TryLoginResult parsed; + + try + { + parsed = result.Body.Value.Deserialize( + new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; + } + catch (JsonException ex) + { + throw new UAuthProtocolException("Invalid try-login result.", ex); + } if (parsed is null) throw new UAuthProtocolException("Invalid try-login result."); diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialDbContext.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialDbContext.cs index 6e40935a..fb5a975d 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialDbContext.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialDbContext.cs @@ -1,10 +1,8 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; -internal sealed class UAuthCredentialDbContext : DbContext +public sealed class UAuthCredentialDbContext : DbContext { public DbSet PasswordCredentials => Set(); @@ -13,52 +11,8 @@ public UAuthCredentialDbContext(DbContextOptions optio { } - protected override void OnModelCreating(ModelBuilder b) + protected override void OnModelCreating(ModelBuilder modelBuilder) { - ConfigurePasswordCredential(b); + UAuthCredentialsModelBuilder.Configure(modelBuilder); } - - private void ConfigurePasswordCredential(ModelBuilder b) - { - b.Entity(e => - { - e.ToTable("UAuth_PasswordCredentials"); - e.HasKey(x => x.Id); - - e.Property(x => x.Version).IsConcurrencyToken(); - - e.Property(x => x.Tenant) - .HasConversion( - v => v.Value, - v => TenantKey.FromInternal(v)) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.UserKey) - .HasConversion( - v => v.Value, - v => UserKey.FromString(v)) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.SecretHash) - .HasMaxLength(512) - .IsRequired(); - - e.Property(x => x.SecurityStamp).IsRequired(); - e.Property(x => x.RevokedAt); - e.Property(x => x.ExpiresAt); - e.Property(x => x.LastUsedAt); - e.Property(x => x.Source).HasMaxLength(128); - e.Property(x => x.CreatedAt).IsRequired(); - e.Property(x => x.UpdatedAt); - e.Property(x => x.DeletedAt); - - e.HasIndex(x => new { x.Tenant, x.Id }).IsUnique(); - e.HasIndex(x => new { x.Tenant, x.UserKey }); - e.HasIndex(x => new { x.Tenant, x.UserKey, x.DeletedAt }); - e.HasIndex(x => new { x.Tenant, x.RevokedAt }); - e.HasIndex(x => new { x.Tenant, x.ExpiresAt }); - }); - } -} \ No newline at end of file +} diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialsModelBuilder.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialsModelBuilder.cs new file mode 100644 index 00000000..67415b62 --- /dev/null +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Data/UAuthCredentialsModelBuilder.cs @@ -0,0 +1,60 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; + +public static class UAuthCredentialsModelBuilder +{ + public static void Configure(ModelBuilder b) + { + ConfigurePasswordCredentials(b); + } + + private static void ConfigurePasswordCredentials(ModelBuilder b) + { + b.Entity(e => + { + e.ToTable("UAuth_PasswordCredentials"); + + e.HasKey(x => x.Id); + + e.Property(x => x.Version) + .IsConcurrencyToken(); + + e.Property(x => x.Tenant) + .HasConversion(v => v.Value, v => TenantKey.FromInternal(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.UserKey) + .HasConversion(v => v.Value, v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.SecretHash) + .HasMaxLength(512) + .IsRequired(); + + e.Property(x => x.SecurityStamp) + .IsRequired(); + + e.Property(x => x.Source) + .HasMaxLength(128); + + e.Property(x => x.CreatedAt).HasUtcDateTimeOffsetConverter().IsRequired(); + e.Property(x => x.UpdatedAt).HasNullableUtcDateTimeOffsetConverter(); + e.Property(x => x.DeletedAt).HasNullableUtcDateTimeOffsetConverter(); + e.Property(x => x.RevokedAt).HasNullableUtcDateTimeOffsetConverter(); + e.Property(x => x.ExpiresAt).HasNullableUtcDateTimeOffsetConverter(); + e.Property(x => x.LastUsedAt).HasNullableUtcDateTimeOffsetConverter(); + + e.HasIndex(x => new { x.Tenant, x.Id }).IsUnique(); + e.HasIndex(x => new { x.Tenant, x.UserKey }); + e.HasIndex(x => new { x.Tenant, x.UserKey, x.DeletedAt }); + e.HasIndex(x => new { x.Tenant, x.RevokedAt }); + e.HasIndex(x => new { x.Tenant, x.ExpiresAt }); + }); + } +} diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs index 23f8b844..7c390a43 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -6,10 +6,14 @@ namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthCredentialsEntityFrameworkCore(this IServiceCollection services, Action configureDb) + public static IServiceCollection AddUltimateAuthCredentialsEntityFrameworkCore(this IServiceCollection services, Action? configureDb = null) where TDbContext : DbContext { - services.AddDbContext(configureDb); - services.AddScoped(); + if (configureDb != null) + { + services.AddDbContext(configureDb); + } + + services.AddScoped>(); return services; } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Projections/PasswordCredentialProjection.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Projections/PasswordCredentialProjection.cs index d25e8edc..ae4a8dee 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Projections/PasswordCredentialProjection.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Projections/PasswordCredentialProjection.cs @@ -3,7 +3,7 @@ namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; -internal sealed class PasswordCredentialProjection +public sealed class PasswordCredentialProjection { public Guid Id { get; set; } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStore.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStore.cs index bf533435..383bd7e0 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStore.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStore.cs @@ -8,20 +8,22 @@ namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; -internal sealed class EfCorePasswordCredentialStore : IPasswordCredentialStore +internal sealed class EfCorePasswordCredentialStore : IPasswordCredentialStore where TDbContext : DbContext { - private readonly UAuthCredentialDbContext _db; + private readonly TDbContext _db; private readonly TenantKey _tenant; - public EfCorePasswordCredentialStore(UAuthCredentialDbContext db, TenantContext tenant) + public EfCorePasswordCredentialStore(TDbContext db, TenantContext tenant) { _db = db; _tenant = tenant.Tenant; } + private DbSet DbSet => _db.Set(); + public async Task ExistsAsync(CredentialKey key, CancellationToken ct = default) { - return await _db.PasswordCredentials + return await DbSet .AnyAsync(x => x.Id == key.Id && x.Tenant == _tenant, @@ -32,14 +34,14 @@ public async Task AddAsync(PasswordCredential credential, CancellationToken ct = { var entity = credential.ToProjection(); - _db.PasswordCredentials.Add(entity); + DbSet.Add(entity); await _db.SaveChangesAsync(ct); } public async Task GetAsync(CredentialKey key, CancellationToken ct = default) { - var entity = await _db.PasswordCredentials + var entity = await DbSet .AsNoTracking() .SingleOrDefaultAsync( x => x.Id == key.Id && @@ -51,7 +53,7 @@ public async Task AddAsync(PasswordCredential credential, CancellationToken ct = public async Task SaveAsync(PasswordCredential credential, long expectedVersion, CancellationToken ct = default) { - var entity = await _db.PasswordCredentials + var entity = await DbSet .SingleOrDefaultAsync(x => x.Id == credential.Id && x.Tenant == _tenant, @@ -71,7 +73,7 @@ public async Task SaveAsync(PasswordCredential credential, long expectedVersion, public async Task RevokeAsync(CredentialKey key, DateTimeOffset revokedAt, long expectedVersion, CancellationToken ct = default) { - var entity = await _db.PasswordCredentials + var entity = await DbSet .SingleOrDefaultAsync(x => x.Id == key.Id && x.Tenant == _tenant, @@ -93,7 +95,7 @@ public async Task RevokeAsync(CredentialKey key, DateTimeOffset revokedAt, long public async Task DeleteAsync(CredentialKey key, long expectedVersion, DeleteMode mode, DateTimeOffset now, CancellationToken ct = default) { - var entity = await _db.PasswordCredentials + var entity = await DbSet .SingleOrDefaultAsync(x => x.Id == key.Id && x.Tenant == _tenant, @@ -107,7 +109,7 @@ public async Task DeleteAsync(CredentialKey key, long expectedVersion, DeleteMod if (mode == DeleteMode.Hard) { - _db.PasswordCredentials.Remove(entity); + DbSet.Remove(entity); } else { @@ -121,7 +123,7 @@ public async Task DeleteAsync(CredentialKey key, long expectedVersion, DeleteMod public async Task> GetByUserAsync(UserKey userKey, CancellationToken ct = default) { - var entities = await _db.PasswordCredentials + var entities = await DbSet .AsNoTracking() .Where(x => x.Tenant == _tenant && @@ -139,7 +141,7 @@ public async Task DeleteByUserAsync(UserKey userKey, DeleteMode mode, DateTimeOf { if (mode == DeleteMode.Hard) { - await _db.PasswordCredentials + await DbSet .Where(x => x.Tenant == _tenant && x.UserKey == userKey) @@ -148,7 +150,7 @@ await _db.PasswordCredentials return; } - await _db.PasswordCredentials + await DbSet .Where(x => x.Tenant == _tenant && x.UserKey == userKey && diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStoreFactory.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStoreFactory.cs index ba037a79..13a0a4a7 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStoreFactory.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/Stores/EfCorePasswordCredentialStoreFactory.cs @@ -1,19 +1,20 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Credentials.Reference; +using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore; -internal sealed class EfCorePasswordCredentialStoreFactory : IPasswordCredentialStoreFactory +internal sealed class EfCorePasswordCredentialStoreFactory : IPasswordCredentialStoreFactory where TDbContext : DbContext { - private readonly UAuthCredentialDbContext _db; + private readonly TDbContext _db; - public EfCorePasswordCredentialStoreFactory(UAuthCredentialDbContext db) + public EfCorePasswordCredentialStoreFactory(TDbContext db) { _db = db; } public IPasswordCredentialStore Create(TenantKey tenant) { - return new EfCorePasswordCredentialStore(_db, new TenantContext(tenant)); + return new EfCorePasswordCredentialStore(_db, new TenantContext(tenant)); } } diff --git a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs index ef75640c..9b53af9c 100644 --- a/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs +++ b/src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/ServiceCollectionExtensions.cs @@ -11,9 +11,6 @@ public static IServiceCollection AddUltimateAuthCredentialsInMemory(this IServic { services.TryAddSingleton(); - // Never try add seed - services.AddSingleton(); - return services; } } diff --git a/src/persistence/CodeBeam.UltimateAuth.EntityFrameworkCore/Infrastructure/DateTimeOffsetConverter.cs b/src/persistence/CodeBeam.UltimateAuth.EntityFrameworkCore/Infrastructure/DateTimeOffsetConverter.cs new file mode 100644 index 00000000..6fd9019e --- /dev/null +++ b/src/persistence/CodeBeam.UltimateAuth.EntityFrameworkCore/Infrastructure/DateTimeOffsetConverter.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace CodeBeam.UltimateAuth.EntityFrameworkCore; + +public static class DateTimeOffsetConverter +{ + public static PropertyBuilder HasUtcDateTimeOffsetConverter(this PropertyBuilder property) + { + return property.HasConversion(UtcDateTimeOffsetConverter); + } + + public static PropertyBuilder HasNullableUtcDateTimeOffsetConverter(this PropertyBuilder property) + { + return property.HasConversion(NullableUtcDateTimeOffsetConverter); + } + + private static readonly ValueConverter UtcDateTimeOffsetConverter = + new( + v => v.UtcDateTime, + v => new DateTimeOffset(DateTime.SpecifyKind(v, DateTimeKind.Utc)) + ); + + private static readonly ValueConverter NullableUtcDateTimeOffsetConverter = + new( + v => v.HasValue ? v.Value.UtcDateTime : null, + v => v.HasValue + ? new DateTimeOffset(DateTime.SpecifyKind(v.Value, DateTimeKind.Utc)) + : null + ); +} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs index 886762d5..93fcd842 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionDbContext.cs @@ -1,173 +1,20 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; -internal sealed class UAuthSessionDbContext : DbContext +public sealed class UAuthSessionDbContext : DbContext { public DbSet Roots => Set(); public DbSet Chains => Set(); public DbSet Sessions => Set(); - public UAuthSessionDbContext(DbContextOptions options) : base(options) + public UAuthSessionDbContext(DbContextOptions options) : base(options) { } - protected override void OnModelCreating(ModelBuilder b) + protected override void OnModelCreating(ModelBuilder modelBuilder) { - b.Entity(e => - { - e.ToTable("UAuth_SessionRoots"); - e.HasKey(x => x.Id); - - e.Property(x => x.Version).IsConcurrencyToken(); - e.Property(x => x.CreatedAt).IsRequired(); - - e.Property(x => x.UserKey) - .HasConversion( - v => v.Value, - v => UserKey.FromString(v)) - .HasMaxLength(128) - .IsRequired(); - e.Property(x => x.Tenant) - .HasConversion( - v => v.Value, - v => TenantKey.FromInternal(v)) - .HasMaxLength(128) - .IsRequired(); - - e.HasIndex(x => new { x.Tenant, x.UserKey }).IsUnique(); - e.HasIndex(x => new { x.Tenant, x.RootId }).IsUnique(); - - e.Property(x => x.SecurityVersion) - .IsRequired(); - - e.Property(x => x.RootId) - .HasConversion( - v => v.Value, - v => SessionRootId.From(v)) - .HasMaxLength(128) - .IsRequired(); - }); - - b.Entity(e => - { - e.ToTable("UAuth_SessionChains"); - e.HasKey(x => x.Id); - - e.Property(x => x.Version).IsConcurrencyToken(); - e.Property(x => x.CreatedAt).IsRequired(); - - e.Property(x => x.UserKey) - .HasConversion( - v => v.Value, - v => UserKey.FromString(v)) - .HasMaxLength(128) - .IsRequired(); - e.Property(x => x.Tenant) - .HasConversion( - v => v.Value, - v => TenantKey.FromInternal(v)) - .HasMaxLength(128) - .IsRequired(); - - e.HasIndex(x => new { x.Tenant, x.ChainId }).IsUnique(); - e.HasIndex(x => new { x.Tenant, x.UserKey }); - e.HasIndex(x => new { x.Tenant, x.UserKey, x.DeviceId }); - e.HasIndex(x => new { x.Tenant, x.RootId }); - - e.HasOne() - .WithMany() - .HasForeignKey(x => new { x.Tenant, x.RootId }) - .HasPrincipalKey(x => new { x.Tenant, x.RootId }) - .OnDelete(DeleteBehavior.Restrict); - - e.Property(x => x.ChainId) - .HasConversion( - v => v.Value, - v => SessionChainId.From(v)) - .IsRequired(); - - e.Property(x => x.DeviceId) - .HasConversion( - v => v.Value, - v => DeviceId.Create(v)) - .HasMaxLength(64) - .IsRequired(); - - e.Property(x => x.Device) - .HasConversion(new JsonValueConverter()) - .IsRequired(); - - e.Property(x => x.ActiveSessionId) - .HasConversion(new NullableAuthSessionIdConverter()); - - e.Property(x => x.ClaimsSnapshot) - .HasConversion(new JsonValueConverter()) - .IsRequired(); - - e.Property(x => x.SecurityVersionAtCreation) - .IsRequired(); - }); - - b.Entity(e => - { - e.ToTable("UAuth_Sessions"); - e.HasKey(x => x.Id); - e.Property(x => x.Version).IsConcurrencyToken(); - e.Property(x => x.CreatedAt).IsRequired(); - - e.Property(x => x.UserKey) - .HasConversion( - v => v.Value, - v => UserKey.FromString(v)) - .HasMaxLength(128) - .IsRequired(); - e.Property(x => x.Tenant) - .HasConversion( - v => v.Value, - v => TenantKey.FromInternal(v)) - .HasMaxLength(128) - .IsRequired(); - - e.HasIndex(x => new { x.Tenant, x.SessionId }).IsUnique(); - e.HasIndex(x => new { x.Tenant, x.ChainId }); - e.HasIndex(x => new { x.Tenant, x.ChainId, x.RevokedAt }); - e.HasIndex(x => new { x.Tenant, x.UserKey, x.RevokedAt }); - e.HasIndex(x => new { x.Tenant, x.ExpiresAt }); - e.HasIndex(x => new { x.Tenant, x.RevokedAt }); - - e.HasOne() - .WithMany() - .HasForeignKey(x => new { x.Tenant, x.ChainId }) - .HasPrincipalKey(x => new { x.Tenant, x.ChainId }) - .OnDelete(DeleteBehavior.Restrict); - - e.Property(x => x.SessionId) - .HasConversion(new AuthSessionIdConverter()) - .IsRequired(); - - e.Property(x => x.ChainId) - .HasConversion( - v => v.Value, - v => SessionChainId.From(v)) - .IsRequired(); - - e.Property(x => x.Device) - .HasConversion(new JsonValueConverter()) - .IsRequired(); - - e.Property(x => x.Claims) - .HasConversion(new JsonValueConverter()) - .IsRequired(); - - e.Property(x => x.Metadata) - .HasConversion(new JsonValueConverter()) - .IsRequired(); - }); + UAuthSessionsModelBuilder.Configure(modelBuilder); } - } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionsModelBuilder.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionsModelBuilder.cs new file mode 100644 index 00000000..e6d98900 --- /dev/null +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Data/UAuthSessionsModelBuilder.cs @@ -0,0 +1,170 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; + +public static class UAuthSessionsModelBuilder +{ + public static void Configure(ModelBuilder b) + { + ConfigureRoots(b); + ConfigureChains(b); + ConfigureSessions(b); + } + + private static void ConfigureRoots(ModelBuilder b) + { + b.Entity(e => + { + e.ToTable("UAuth_SessionRoots"); + e.HasKey(x => x.Id); + + e.Property(x => x.Version).IsConcurrencyToken(); + + e.Property(x => x.CreatedAt).HasUtcDateTimeOffsetConverter().IsRequired(); + e.Property(x => x.UpdatedAt).HasNullableUtcDateTimeOffsetConverter(); + e.Property(x => x.RevokedAt).HasNullableUtcDateTimeOffsetConverter(); + + e.Property(x => x.UserKey) + .HasConversion(v => v.Value, v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.Tenant) + .HasConversion(v => v.Value, v => TenantKey.FromInternal(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.RootId) + .HasConversion(v => v.Value, v => SessionRootId.From(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.SecurityVersion) + .IsRequired(); + + e.HasIndex(x => new { x.Tenant, x.UserKey }).IsUnique(); + e.HasIndex(x => new { x.Tenant, x.RootId }).IsUnique(); + }); + } + + private static void ConfigureChains(ModelBuilder b) + { + b.Entity(e => + { + e.ToTable("UAuth_SessionChains"); + e.HasKey(x => x.Id); + + e.Property(x => x.Version).IsConcurrencyToken(); + + e.Property(x => x.CreatedAt).HasUtcDateTimeOffsetConverter().IsRequired(); + e.Property(x => x.LastSeenAt).HasUtcDateTimeOffsetConverter(); + e.Property(x => x.RevokedAt).HasNullableUtcDateTimeOffsetConverter(); + e.Property(x => x.AbsoluteExpiresAt).HasNullableUtcDateTimeOffsetConverter(); + + e.Property(x => x.UserKey) + .HasConversion(v => v.Value, v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.Tenant) + .HasConversion(v => v.Value, v => TenantKey.FromInternal(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.ChainId) + .HasConversion(v => v.Value, v => SessionChainId.From(v)) + .IsRequired(); + + e.Property(x => x.DeviceId) + .HasConversion(v => v.Value, v => DeviceId.Create(v)) + .HasMaxLength(64) + .IsRequired(); + + e.Property(x => x.Device) + .HasConversion(new JsonValueConverter()) + .IsRequired(); + + e.Property(x => x.ActiveSessionId) + .HasConversion(new NullableAuthSessionIdConverter()); + + e.Property(x => x.ClaimsSnapshot) + .HasConversion(new JsonValueConverter()) + .IsRequired(); + + e.Property(x => x.SecurityVersionAtCreation) + .IsRequired(); + + e.HasIndex(x => new { x.Tenant, x.ChainId }).IsUnique(); + e.HasIndex(x => new { x.Tenant, x.UserKey }); + e.HasIndex(x => new { x.Tenant, x.UserKey, x.DeviceId }); + e.HasIndex(x => new { x.Tenant, x.RootId }); + + e.HasOne() + .WithMany() + .HasForeignKey(x => new { x.Tenant, x.RootId }) + .HasPrincipalKey(x => new { x.Tenant, x.RootId }) + .OnDelete(DeleteBehavior.Restrict); + }); + } + + private static void ConfigureSessions(ModelBuilder b) + { + b.Entity(e => + { + e.ToTable("UAuth_Sessions"); + e.HasKey(x => x.Id); + + e.Property(x => x.Version).IsConcurrencyToken(); + + e.Property(x => x.CreatedAt).HasUtcDateTimeOffsetConverter().IsRequired(); + e.Property(x => x.ExpiresAt).HasUtcDateTimeOffsetConverter().IsRequired(); + e.Property(x => x.RevokedAt).HasNullableUtcDateTimeOffsetConverter(); + + e.Property(x => x.UserKey) + .HasConversion(v => v.Value, v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.Tenant) + .HasConversion(v => v.Value, v => TenantKey.FromInternal(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.SessionId) + .HasConversion(new AuthSessionIdConverter()) + .IsRequired(); + + e.Property(x => x.ChainId) + .HasConversion(v => v.Value, v => SessionChainId.From(v)) + .IsRequired(); + + e.Property(x => x.Device) + .HasConversion(new JsonValueConverter()) + .IsRequired(); + + e.Property(x => x.Claims) + .HasConversion(new JsonValueConverter()) + .IsRequired(); + + e.Property(x => x.Metadata) + .HasConversion(new JsonValueConverter()) + .IsRequired(); + + e.HasIndex(x => new { x.Tenant, x.SessionId }).IsUnique(); + e.HasIndex(x => new { x.Tenant, x.ChainId }); + e.HasIndex(x => new { x.Tenant, x.ChainId, x.RevokedAt }); + e.HasIndex(x => new { x.Tenant, x.UserKey, x.RevokedAt }); + e.HasIndex(x => new { x.Tenant, x.ExpiresAt }); + e.HasIndex(x => new { x.Tenant, x.RevokedAt }); + + e.HasOne() + .WithMany() + .HasForeignKey(x => new { x.Tenant, x.ChainId }) + .HasPrincipalKey(x => new { x.Tenant, x.ChainId }) + .OnDelete(DeleteBehavior.Restrict); + }); + } +} diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs index 69762efe..2dc070af 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -6,10 +6,14 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthSessionsEntityFrameworkCore(this IServiceCollection services,Action configureDb) + public static IServiceCollection AddUltimateAuthSessionsEntityFrameworkCore(this IServiceCollection services, Action? configureDb = null) where TDbContext : DbContext { - services.AddDbContext(configureDb); - services.AddScoped(); + if (configureDb != null) + { + services.AddDbContext(configureDb); + } + + services.AddScoped>(); return services; } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Projections/SessionChainProjection.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Projections/SessionChainProjection.cs index ffc23f10..c9f417a9 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Projections/SessionChainProjection.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Projections/SessionChainProjection.cs @@ -3,7 +3,7 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; -internal sealed class SessionChainProjection +public sealed class SessionChainProjection { public long Id { get; set; } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Projections/SessionProjection.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Projections/SessionProjection.cs index 19061c50..7860d578 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Projections/SessionProjection.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Projections/SessionProjection.cs @@ -3,20 +3,17 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; -internal sealed class SessionProjection +public sealed class SessionProjection { public long Id { get; set; } // EF internal PK public AuthSessionId SessionId { get; set; } = default!; public SessionChainId ChainId { get; set; } = default!; - public TenantKey Tenant { get; set; } public UserKey UserKey { get; set; } = default!; public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset ExpiresAt { get; set; } - - public DateTimeOffset? RevokedAt { get; set; } public long SecurityVersionAtCreation { get; set; } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Projections/SessionRootProjection.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Projections/SessionRootProjection.cs index 4d9c0ff8..c6ae69d9 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Projections/SessionRootProjection.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Projections/SessionRootProjection.cs @@ -3,7 +3,7 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; -internal sealed class SessionRootProjection +public sealed class SessionRootProjection { public long Id { get; set; } public SessionRootId RootId { get; set; } diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs index c9a31e6e..ca86ff54 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStore.cs @@ -7,17 +7,21 @@ namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; -internal sealed class EfCoreSessionStore : ISessionStore +internal sealed class EfCoreSessionStore : ISessionStore where TDbContext : DbContext { - private readonly UAuthSessionDbContext _db; + private readonly TDbContext _db; private readonly TenantKey _tenant; - public EfCoreSessionStore(UAuthSessionDbContext db, TenantContext tenant) + public EfCoreSessionStore(TDbContext db, TenantContext tenant) { _db = db; _tenant = tenant.Tenant; } + private DbSet DbSetSession => _db.Set(); + private DbSet DbSetChain => _db.Set(); + private DbSet DbSetRoot => _db.Set(); + public async Task ExecuteAsync(Func action, CancellationToken ct = default) { var strategy = _db.Database.CreateExecutionStrategy(); @@ -77,7 +81,12 @@ public async Task ExecuteAsync(Func x.Tenant == _tenant && x.SessionId == sessionId); + + if (local != null) + return local.ToDomain(); + + var projection = await DbSetSession .AsNoTracking() .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.SessionId == sessionId); @@ -88,11 +97,16 @@ public async Task SaveSessionAsync(UAuthSession session, long expectedVersion, C { ct.ThrowIfCancellationRequested(); - var projection = await _db.Sessions + var projection = DbSetSession.Local.FirstOrDefault(x => x.Tenant == _tenant && x.SessionId == session.SessionId); + + if (projection == null) + { + projection = await DbSetSession .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.SessionId == session.SessionId, ct); + } if (projection is null) throw new UAuthNotFoundException("session_not_found"); @@ -113,7 +127,7 @@ public Task CreateSessionAsync(UAuthSession session, CancellationToken ct = defa if (session.Version != 0) throw new InvalidOperationException("New session must have version 0."); - _db.Sessions.Add(projection); + DbSetSession.Add(projection); return Task.CompletedTask; } @@ -122,7 +136,7 @@ public async Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffs { ct.ThrowIfCancellationRequested(); - var projection = await _db.Sessions.SingleOrDefaultAsync(x => x.Tenant == _tenant && x.SessionId == sessionId, ct); + var projection = await DbSetSession.SingleOrDefaultAsync(x => x.Tenant == _tenant && x.SessionId == sessionId, ct); if (projection is null || projection.RevokedAt is not null) return false; @@ -138,13 +152,13 @@ public async Task RevokeAllSessionsAsync(UserKey user, DateTimeOffset at, Cancel { ct.ThrowIfCancellationRequested(); - var chains = await _db.Chains + var chains = await DbSetChain .Where(x => x.Tenant == _tenant && x.UserKey == user) .ToListAsync(ct); var chainIds = chains.Select(x => x.ChainId).ToList(); - var sessions = await _db.Sessions + var sessions = await DbSetSession .Where(x => x.Tenant == _tenant && chainIds.Contains(x.ChainId)) .ToListAsync(ct); @@ -174,13 +188,13 @@ public async Task RevokeOtherSessionsAsync(UserKey user, SessionChainId keepChai { ct.ThrowIfCancellationRequested(); - var chains = await _db.Chains + var chains = await DbSetChain .Where(x => x.Tenant == _tenant && x.UserKey == user && x.ChainId != keepChain) .ToListAsync(ct); var chainIds = chains.Select(x => x.ChainId).ToList(); - var sessions = await _db.Sessions + var sessions = await DbSetSession .Where(x => x.Tenant == _tenant && chainIds.Contains(x.ChainId)) .ToListAsync(ct); @@ -210,9 +224,14 @@ public async Task RevokeOtherSessionsAsync(UserKey user, SessionChainId keepChai { ct.ThrowIfCancellationRequested(); - var projection = await _db.Chains + var local = DbSetChain.Local.FirstOrDefault(x => x.Tenant == _tenant && x.ChainId == chainId); + + if (local is not null) + return local.ToDomain(); + + var projection = await DbSetChain .AsNoTracking() - .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId); + .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId, ct); return projection?.ToDomain(); } @@ -221,14 +240,25 @@ public async Task RevokeOtherSessionsAsync(UserKey user, SessionChainId keepChai { ct.ThrowIfCancellationRequested(); - var projection = await _db.Chains + var local = DbSetChain.Local + .Where(x => + x.Tenant == _tenant && + x.UserKey == userKey && + x.RevokedAt == null && + x.DeviceId == deviceId) + .FirstOrDefault(); + + if (local != null) + return local.ToDomain(); + + var projection = await DbSetChain .AsNoTracking() .Where(x => x.Tenant == _tenant && x.UserKey == userKey && x.RevokedAt == null && x.DeviceId == deviceId) - .SingleOrDefaultAsync(ct); + .FirstOrDefaultAsync(ct); return projection?.ToDomain(); } @@ -237,11 +267,13 @@ public async Task SaveChainAsync(UAuthSessionChain chain, long expectedVersion, { ct.ThrowIfCancellationRequested(); - var projection = await _db.Chains - .SingleOrDefaultAsync(x => - x.Tenant == _tenant && - x.ChainId == chain.ChainId, - ct); + var projection = DbSetChain.Local.FirstOrDefault(x => x.Tenant == _tenant && x.ChainId == chain.ChainId); + + if (projection is null) + { + projection = await DbSetChain + .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chain.ChainId, ct); + } if (projection is null) throw new UAuthNotFoundException("chain_not_found"); @@ -262,7 +294,8 @@ public Task CreateChainAsync(UAuthSessionChain chain, CancellationToken ct = def var projection = chain.ToProjection(); - _db.Chains.Add(projection); + DbSetChain.Add(projection); + _db.Entry(projection).State = EntityState.Added; return Task.CompletedTask; } @@ -271,7 +304,7 @@ public async Task RevokeChainAsync(SessionChainId chainId, DateTimeOffset at, Ca { ct.ThrowIfCancellationRequested(); - var projection = await _db.Chains + var projection = await DbSetChain .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId, ct); if (projection is null || projection.RevokedAt is not null) @@ -286,13 +319,13 @@ public async Task LogoutChainAsync(SessionChainId chainId, DateTimeOffset at, Ca { ct.ThrowIfCancellationRequested(); - var chainProjection = await _db.Chains + var chainProjection = await DbSetChain .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId, ct); if (chainProjection is null || chainProjection.RevokedAt is not null) return; - var sessions = await _db.Sessions + var sessions = await DbSetSession .Where(x => x.Tenant == _tenant && x.ChainId == chainId) .ToListAsync(ct); @@ -319,7 +352,7 @@ public async Task RevokeOtherChainsAsync(UserKey userKey, SessionChainId current { ct.ThrowIfCancellationRequested(); - var projections = await _db.Chains + var projections = await DbSetChain .Where(x => x.Tenant == _tenant && x.UserKey == userKey && @@ -339,7 +372,7 @@ public async Task RevokeAllChainsAsync(UserKey userKey, DateTimeOffset at, Cance { ct.ThrowIfCancellationRequested(); - var projections = await _db.Chains + var projections = await DbSetChain .Where(x => x.Tenant == _tenant && x.UserKey == userKey && @@ -358,7 +391,7 @@ public async Task RevokeAllChainsAsync(UserKey userKey, DateTimeOffset at, Cance { ct.ThrowIfCancellationRequested(); - return await _db.Chains + return await DbSetChain .AsNoTracking() .Where(x => x.Tenant == _tenant && x.ChainId == chainId) .Select(x => x.ActiveSessionId) @@ -369,17 +402,15 @@ public async Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId { ct.ThrowIfCancellationRequested(); - var projection = _db.Chains.Local - .FirstOrDefault(x => x.Tenant == _tenant && x.ChainId == chainId); + var projection = DbSetChain.Local.FirstOrDefault(x => x.Tenant == _tenant && x.ChainId == chainId); if (projection is null) { - projection = await _db.Chains - .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId, ct); + projection = await DbSetChain.SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId, ct); } if (projection is null) - return; + throw new UAuthNotFoundException("chain_not_found"); projection.ActiveSessionId = sessionId; projection.Version++; @@ -389,7 +420,7 @@ public async Task SetActiveSessionIdAsync(SessionChainId chainId, AuthSessionId { ct.ThrowIfCancellationRequested(); - var rootProjection = await _db.Roots.AsNoTracking().SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == userKey, ct); + var rootProjection = await DbSetRoot.AsNoTracking().SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == userKey, ct); return rootProjection?.ToDomain(); } @@ -397,7 +428,7 @@ public async Task SaveRootAsync(UAuthSessionRoot root, long expectedVersion, Can { ct.ThrowIfCancellationRequested(); - var projection = await _db.Roots + var projection = await DbSetRoot .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == root.UserKey, @@ -422,7 +453,7 @@ public Task CreateRootAsync(UAuthSessionRoot root, CancellationToken ct = defaul var projection = root.ToProjection(); - _db.Roots.Add(projection); + DbSetRoot.Add(projection); return Task.CompletedTask; } @@ -431,7 +462,7 @@ public async Task RevokeRootAsync(UserKey userKey, DateTimeOffset at, Cancellati { ct.ThrowIfCancellationRequested(); - var projection = await _db.Roots + var projection = await DbSetRoot .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == userKey, ct); if (projection is null || projection.RevokedAt is not null) @@ -446,7 +477,7 @@ public async Task RevokeRootAsync(UserKey userKey, DateTimeOffset at, Cancellati { ct.ThrowIfCancellationRequested(); - return await _db.Sessions + return await DbSetSession .AsNoTracking() .Where(x => x.Tenant == _tenant && x.SessionId == sessionId) .Select(x => (SessionChainId?)x.ChainId) @@ -457,7 +488,7 @@ public async Task> GetChainsByUserAsync(UserKey { ct.ThrowIfCancellationRequested(); - var rootsQuery = _db.Roots.AsNoTracking().Where(x => x.Tenant == _tenant && x.UserKey == userKey); + var rootsQuery = DbSetRoot.AsNoTracking().Where(x => x.Tenant == _tenant && x.UserKey == userKey); if (!includeHistoricalRoots) { @@ -469,7 +500,7 @@ public async Task> GetChainsByUserAsync(UserKey if (rootIds.Count == 0) return Array.Empty(); - var projections = await _db.Chains.AsNoTracking().Where(x => x.Tenant == _tenant && rootIds.Contains(x.RootId)).ToListAsync(); + var projections = await DbSetChain.AsNoTracking().Where(x => x.Tenant == _tenant && rootIds.Contains(x.RootId)).ToListAsync(); return projections.Select(c => c.ToDomain()).ToList(); } @@ -477,7 +508,7 @@ public async Task> GetChainsByRootAsync(Session { ct.ThrowIfCancellationRequested(); - var projections = await _db.Chains + var projections = await DbSetChain .AsNoTracking() .Where(x => x.Tenant == _tenant && x.RootId == rootId) .ToListAsync(); @@ -489,7 +520,7 @@ public async Task> GetSessionsByChainAsync(SessionCh { ct.ThrowIfCancellationRequested(); - var projections = await _db.Sessions + var projections = await DbSetSession .AsNoTracking() .Where(x => x.Tenant == _tenant && x.ChainId == chainId) .ToListAsync(); @@ -501,7 +532,7 @@ public async Task> GetSessionsByChainAsync(SessionCh { ct.ThrowIfCancellationRequested(); - var projection = await _db.Roots.AsNoTracking().SingleOrDefaultAsync(x => x.Tenant == _tenant && x.RootId == rootId, ct); + var projection = await DbSetRoot.AsNoTracking().SingleOrDefaultAsync(x => x.Tenant == _tenant && x.RootId == rootId, ct); return projection?.ToDomain(); } @@ -509,25 +540,25 @@ public async Task RemoveSessionAsync(AuthSessionId sessionId, CancellationToken { ct.ThrowIfCancellationRequested(); - var projection = await _db.Sessions.SingleOrDefaultAsync(x => x.Tenant == _tenant && x.SessionId == sessionId, ct); + var projection = await DbSetSession.SingleOrDefaultAsync(x => x.Tenant == _tenant && x.SessionId == sessionId, ct); if (projection is null) return; - _db.Sessions.Remove(projection); + DbSetSession.Remove(projection); } public async Task RevokeChainCascadeAsync(SessionChainId chainId, DateTimeOffset at, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var chainProjection = await _db.Chains + var chainProjection = await DbSetChain .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.ChainId == chainId, ct); if (chainProjection is null) return; - var sessionProjections = await _db.Sessions + var sessionProjections = await DbSetSession .Where(x => x.Tenant == _tenant && x.ChainId == chainId && x.RevokedAt == null) .ToListAsync(ct); @@ -550,19 +581,19 @@ public async Task RevokeRootCascadeAsync(UserKey userKey, DateTimeOffset at, Can { ct.ThrowIfCancellationRequested(); - var rootProjection = await _db.Roots + var rootProjection = await DbSetRoot .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == userKey, ct); if (rootProjection is null) return; - var chainProjections = await _db.Chains + var chainProjections = await DbSetChain .Where(x => x.Tenant == _tenant && x.UserKey == userKey) .ToListAsync(ct); foreach (var chainProjection in chainProjections) { - var sessions = await _db.Sessions + var sessions = await DbSetSession .Where(x => x.Tenant == _tenant && x.ChainId == chainProjection.ChainId) .ToListAsync(ct); diff --git a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreFactory.cs b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreFactory.cs index b64206f1..363e3738 100644 --- a/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreFactory.cs +++ b/src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/Stores/EfCoreSessionStoreFactory.cs @@ -1,19 +1,20 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore; -internal sealed class EfCoreSessionStoreFactory : ISessionStoreFactory +internal sealed class EfCoreSessionStoreFactory : ISessionStoreFactory where TDbContext : DbContext { - private readonly UAuthSessionDbContext _db; + private readonly TDbContext _db; - public EfCoreSessionStoreFactory(UAuthSessionDbContext db) + public EfCoreSessionStoreFactory(TDbContext db) { _db = db; } public ISessionStore Create(TenantKey tenant) { - return new EfCoreSessionStore(_db, new TenantContext(tenant)); + return new EfCoreSessionStore(_db, new TenantContext(tenant)); } } diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Data/UAuthTokenDbContext.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Data/UAuthTokenDbContext.cs index 304b7a74..032fc0cd 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Data/UAuthTokenDbContext.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Data/UAuthTokenDbContext.cs @@ -1,11 +1,8 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; -internal sealed class UAuthTokenDbContext : DbContext +public sealed class UAuthTokenDbContext : DbContext { public DbSet RefreshTokens => Set(); //public DbSet RevokedTokenIds => Set(); // TODO: Add when JWT added. @@ -15,57 +12,8 @@ public UAuthTokenDbContext(DbContextOptions options) { } - protected override void OnModelCreating(ModelBuilder b) + protected override void OnModelCreating(ModelBuilder modelBuilder) { - b.Entity(e => - { - e.ToTable("UAuth_RefreshTokens"); - e.HasKey(x => x.Id); - - e.Property(x => x.Version) - .IsConcurrencyToken(); - - e.Property(x => x.Tenant) - .HasConversion( - v => v.Value, - v => TenantKey.FromInternal(v)) - .HasMaxLength(128) - .IsRequired(); - e.Property(x => x.UserKey) - .HasConversion( - v => v.Value, - v => UserKey.FromString(v)) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.TokenId) - .HasConversion( - v => v.Value, - v => TokenId.From(v)) - .IsRequired(); - - e.Property(x => x.TokenHash) - .HasMaxLength(128) - .IsRequired(); - - e.HasIndex(x => new { x.Tenant, x.TokenHash }).IsUnique(); - e.HasIndex(x => new { x.Tenant, x.TokenHash, x.RevokedAt }); - e.HasIndex(x => new { x.Tenant, x.TokenId }); - e.HasIndex(x => new { x.Tenant, x.UserKey }); - e.HasIndex(x => new { x.Tenant, x.SessionId }); - e.HasIndex(x => new { x.Tenant, x.ChainId }); - e.HasIndex(x => new { x.Tenant, x.ExpiresAt }); - e.HasIndex(x => new { x.Tenant, x.ExpiresAt, x.RevokedAt }); - e.HasIndex(x => new { x.Tenant, x.ReplacedByTokenHash }); - - e.Property(x => x.SessionId) - .HasConversion(new AuthSessionIdConverter()); - - e.Property(x => x.ChainId) - .HasConversion(new NullableSessionChainIdConverter()); - - e.Property(x => x.ExpiresAt) - .IsRequired(); - }); + UAuthTokensModelBuilder.Configure(modelBuilder); } } diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Data/UAuthTokenModelBuilder.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Data/UAuthTokenModelBuilder.cs new file mode 100644 index 00000000..f6f1b026 --- /dev/null +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Data/UAuthTokenModelBuilder.cs @@ -0,0 +1,73 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; + +public static class UAuthTokensModelBuilder +{ + public static void Configure(ModelBuilder b) + { + ConfigureRefreshTokens(b); + } + + private static void ConfigureRefreshTokens(ModelBuilder b) + { + b.Entity(e => + { + e.ToTable("UAuth_RefreshTokens"); + + e.HasKey(x => x.Id); + + e.Property(x => x.Version).IsConcurrencyToken(); + + e.Property(x => x.Tenant) + .HasConversion( + v => v.Value, + v => TenantKey.FromInternal(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.UserKey) + .HasConversion( + v => v.Value, + v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.TokenId) + .HasConversion( + v => v.Value, + v => TokenId.From(v)) + .IsRequired(); + + e.Property(x => x.TokenHash) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.SessionId) + .HasConversion(new AuthSessionIdConverter()); + + e.Property(x => x.ChainId) + .HasConversion(new NullableSessionChainIdConverter()); + + e.Property(x => x.CreatedAt).HasUtcDateTimeOffsetConverter().IsRequired(); + e.Property(x => x.ExpiresAt).HasUtcDateTimeOffsetConverter().IsRequired(); + e.Property(x => x.RevokedAt).HasNullableUtcDateTimeOffsetConverter(); + + e.Property(x => x.ReplacedByTokenHash) + .HasMaxLength(128); + + e.HasIndex(x => new { x.Tenant, x.TokenHash }).IsUnique(); + e.HasIndex(x => new { x.Tenant, x.TokenHash, x.RevokedAt }); + e.HasIndex(x => new { x.Tenant, x.TokenId }); + e.HasIndex(x => new { x.Tenant, x.UserKey }); + e.HasIndex(x => new { x.Tenant, x.SessionId }); + e.HasIndex(x => new { x.Tenant, x.ChainId }); + e.HasIndex(x => new { x.Tenant, x.ExpiresAt }); + e.HasIndex(x => new { x.Tenant, x.ExpiresAt, x.RevokedAt }); + e.HasIndex(x => new { x.Tenant, x.ReplacedByTokenHash }); + }); + } +} \ No newline at end of file diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs index 66a01476..11410052 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -6,10 +6,14 @@ namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthTokensEntityFrameworkCore(this IServiceCollection services, Action configureDb) + public static IServiceCollection AddUltimateAuthTokensEntityFrameworkCore(this IServiceCollection services, Action? configureDb = null) where TDbContext : DbContext { - services.AddDbContext(configureDb); - services.AddScoped(); + if (configureDb != null) + { + services.AddDbContext(configureDb); + } + + services.AddScoped>(); return services; } } diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Projections/RefreshTokenProjection.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Projections/RefreshTokenProjection.cs index 3f47970e..2e67b489 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Projections/RefreshTokenProjection.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Projections/RefreshTokenProjection.cs @@ -3,7 +3,7 @@ namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; -internal sealed class RefreshTokenProjection +public sealed class RefreshTokenProjection { public long Id { get; set; } // EF PK @@ -22,9 +22,7 @@ internal sealed class RefreshTokenProjection public string? ReplacedByTokenHash { get; set; } public DateTimeOffset CreatedAt { get; set; } - public DateTimeOffset ExpiresAt { get; set; } - public DateTimeOffset? RevokedAt { get; set; } public long Version { get; set; } diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStore.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStore.cs index 6d4aad70..b4be2ef5 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStore.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStore.cs @@ -5,18 +5,20 @@ namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; -internal sealed class EfCoreRefreshTokenStore : IRefreshTokenStore +internal sealed class EfCoreRefreshTokenStore : IRefreshTokenStore where TDbContext : DbContext { - private readonly UAuthTokenDbContext _db; + private readonly TDbContext _db; private readonly TenantKey _tenant; private bool _inTransaction; - public EfCoreRefreshTokenStore(UAuthTokenDbContext db, TenantContext tenant) + public EfCoreRefreshTokenStore(TDbContext db, TenantContext tenant) { _db = db; _tenant = tenant.Tenant; } + private DbSet DbSet => _db.Set(); + public async Task ExecuteAsync(Func action, CancellationToken ct = default) { var strategy = _db.Database.CreateExecutionStrategy(); @@ -88,7 +90,7 @@ public Task StoreAsync(RefreshToken token, CancellationToken ct = default) if (token.Tenant != _tenant) throw new InvalidOperationException("Tenant mismatch."); - _db.RefreshTokens.Add(token.ToProjection()); + DbSet.Add(token.ToProjection()); return Task.CompletedTask; } @@ -97,7 +99,7 @@ public Task StoreAsync(RefreshToken token, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var p = await _db.RefreshTokens + var p = await DbSet .AsNoTracking() .SingleOrDefaultAsync( x => x.Tenant == _tenant && @@ -112,7 +114,7 @@ public Task RevokeAsync(string tokenHash, DateTimeOffset revokedAt, string? repl ct.ThrowIfCancellationRequested(); EnsureTransaction(); - var query = _db.RefreshTokens + var query = DbSet .Where(x => x.Tenant == _tenant && x.TokenHash == tokenHash && @@ -137,7 +139,7 @@ public Task RevokeBySessionAsync(AuthSessionId sessionId, DateTimeOffset revoked ct.ThrowIfCancellationRequested(); EnsureTransaction(); - return _db.RefreshTokens + return DbSet .Where(x => x.Tenant == _tenant && x.SessionId == sessionId && @@ -152,7 +154,7 @@ public Task RevokeByChainAsync(SessionChainId chainId, DateTimeOffset revokedAt, ct.ThrowIfCancellationRequested(); EnsureTransaction(); - return _db.RefreshTokens + return DbSet .Where(x => x.Tenant == _tenant && x.ChainId == chainId && @@ -167,7 +169,7 @@ public Task RevokeAllForUserAsync(UserKey userKey, DateTimeOffset revokedAt, Can ct.ThrowIfCancellationRequested(); EnsureTransaction(); - return _db.RefreshTokens + return DbSet .Where(x => x.Tenant == _tenant && x.UserKey == userKey && diff --git a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStoreFactory.cs b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStoreFactory.cs index f584beae..9cc94371 100644 --- a/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStoreFactory.cs +++ b/src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStoreFactory.cs @@ -1,19 +1,20 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.MultiTenancy; +using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore; -internal sealed class EfCoreRefreshTokenStoreFactory : IRefreshTokenStoreFactory +internal sealed class EfCoreRefreshTokenStoreFactory : IRefreshTokenStoreFactory where TDbContext : DbContext { - private readonly UAuthTokenDbContext _db; + private readonly TDbContext _db; - public EfCoreRefreshTokenStoreFactory(UAuthTokenDbContext db) + public EfCoreRefreshTokenStoreFactory(TDbContext db) { _db = db; } public IRefreshTokenStore Create(TenantKey tenant) { - return new EfCoreRefreshTokenStore(_db, new TenantContext(tenant)); + return new EfCoreRefreshTokenStore(_db, new TenantContext(tenant)); } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUserDbContext.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUserDbContext.cs index f7f0d05c..88dbd3f2 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUserDbContext.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUserDbContext.cs @@ -1,14 +1,11 @@ -using CodeBeam.UltimateAuth.Core.Domain; -using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; -internal sealed class UAuthUserDbContext : DbContext +public sealed class UAuthUserDbContext : DbContext { - public DbSet Identifiers => Set(); public DbSet Lifecycles => Set(); + public DbSet Identifiers => Set(); public DbSet Profiles => Set(); public UAuthUserDbContext(DbContextOptions options) @@ -16,122 +13,8 @@ public UAuthUserDbContext(DbContextOptions options) { } - protected override void OnModelCreating(ModelBuilder b) - { - ConfigureIdentifiers(b); - ConfigureLifecycles(b); - ConfigureProfiles(b); - } - - private void ConfigureIdentifiers(ModelBuilder b) - { - b.Entity(e => - { - e.ToTable("UAuth_UserIdentifiers"); - e.HasKey(x => x.Id); - - e.Property(x => x.Version) - .IsConcurrencyToken(); - - e.Property(x => x.Tenant) - .HasConversion( - v => v.Value, - v => TenantKey.FromInternal(v)) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.UserKey) - .HasConversion( - v => v.Value, - v => UserKey.FromString(v)) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.Value) - .HasMaxLength(256) - .IsRequired(); - - e.Property(x => x.NormalizedValue) - .HasMaxLength(256) - .IsRequired(); - - e.HasIndex(x => new { x.Tenant, x.Type, x.NormalizedValue }).IsUnique(); - e.HasIndex(x => new { x.Tenant, x.UserKey }); - e.HasIndex(x => new { x.Tenant, x.UserKey, x.Type, x.IsPrimary }); - e.HasIndex(x => new { x.Tenant, x.UserKey, x.IsPrimary }); - e.HasIndex(x => new { x.Tenant, x.NormalizedValue }); - - e.Property(x => x.CreatedAt) - .IsRequired(); - }); - } - - private void ConfigureLifecycles(ModelBuilder b) - { - b.Entity(e => - { - e.ToTable("UAuth_UserLifecycles"); - e.HasKey(x => x.Id); - - e.Property(x => x.Version) - .IsConcurrencyToken(); - - e.Property(x => x.Tenant) - .HasConversion( - v => v.Value, - v => TenantKey.FromInternal(v)) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.UserKey) - .HasConversion( - v => v.Value, - v => UserKey.FromString(v)) - .HasMaxLength(128) - .IsRequired(); - - e.HasIndex(x => new { x.Tenant, x.UserKey }).IsUnique(); - - e.Property(x => x.SecurityVersion) - .IsRequired(); - - e.Property(x => x.CreatedAt) - .IsRequired(); - }); - } - - private void ConfigureProfiles(ModelBuilder b) + protected override void OnModelCreating(ModelBuilder modelBuilder) { - b.Entity(e => - { - e.ToTable("UAuth_UserProfiles"); - e.HasKey(x => x.Id); - - e.Property(x => x.Version) - .IsConcurrencyToken(); - - e.Property(x => x.Tenant) - .HasConversion( - v => v.Value, - v => TenantKey.FromInternal(v)) - .HasMaxLength(128) - .IsRequired(); - - e.Property(x => x.UserKey) - .HasConversion( - v => v.Value, - v => UserKey.FromString(v)) - .HasMaxLength(128) - .IsRequired(); - - e.HasIndex(x => new { x.Tenant, x.UserKey }); - - e.Property(x => x.Metadata) - .HasConversion(new NullableJsonValueConverter>()) - .Metadata.SetValueComparer(JsonValueComparers.Create>()); - - e.Property(x => x.CreatedAt) - .IsRequired(); - }); + UAuthUsersModelBuilder.Configure(modelBuilder); } -} \ No newline at end of file +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs new file mode 100644 index 00000000..edbf278c --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs @@ -0,0 +1,122 @@ +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; + +public static class UAuthUsersModelBuilder +{ + public static void Configure(ModelBuilder b) + { + ConfigureIdentifiers(b); + ConfigureLifecycles(b); + ConfigureProfiles(b); + } + + private static void ConfigureIdentifiers(ModelBuilder b) + { + b.Entity(e => + { + e.ToTable("UAuth_UserIdentifiers"); + + e.HasKey(x => x.Id); + + e.Property(x => x.Version) + .IsConcurrencyToken(); + + e.Property(x => x.Tenant) + .HasConversion(v => v.Value, v => TenantKey.FromInternal(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.UserKey) + .HasConversion(v => v.Value, v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.Value) + .HasMaxLength(256) + .IsRequired(); + + e.Property(x => x.NormalizedValue) + .HasMaxLength(256) + .IsRequired(); + + e.Property(x => x.CreatedAt).HasUtcDateTimeOffsetConverter().IsRequired(); + e.Property(x => x.UpdatedAt).HasNullableUtcDateTimeOffsetConverter(); + e.Property(x => x.DeletedAt).HasNullableUtcDateTimeOffsetConverter(); + + e.HasIndex(x => new { x.Tenant, x.Type, x.NormalizedValue }).IsUnique(); + e.HasIndex(x => new { x.Tenant, x.UserKey }); + e.HasIndex(x => new { x.Tenant, x.UserKey, x.Type, x.IsPrimary }); + e.HasIndex(x => new { x.Tenant, x.UserKey, x.IsPrimary }); + e.HasIndex(x => new { x.Tenant, x.NormalizedValue }); + }); + } + + private static void ConfigureLifecycles(ModelBuilder b) + { + b.Entity(e => + { + e.ToTable("UAuth_UserLifecycles"); + + e.HasKey(x => x.Id); + + e.Property(x => x.Version) + .IsConcurrencyToken(); + + e.Property(x => x.Tenant) + .HasConversion(v => v.Value, v => TenantKey.FromInternal(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.UserKey) + .HasConversion(v => v.Value, v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.CreatedAt).HasUtcDateTimeOffsetConverter().IsRequired(); + e.Property(x => x.UpdatedAt).HasNullableUtcDateTimeOffsetConverter(); + e.Property(x => x.DeletedAt).HasNullableUtcDateTimeOffsetConverter(); + + e.Property(x => x.SecurityVersion) + .IsRequired(); + + e.HasIndex(x => new { x.Tenant, x.UserKey }).IsUnique(); + }); + } + + private static void ConfigureProfiles(ModelBuilder b) + { + b.Entity(e => + { + e.ToTable("UAuth_UserProfiles"); + + e.HasKey(x => x.Id); + + e.Property(x => x.Version) + .IsConcurrencyToken(); + + e.Property(x => x.Tenant) + .HasConversion(v => v.Value, v => TenantKey.FromInternal(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.UserKey) + .HasConversion(v => v.Value, v => UserKey.FromString(v)) + .HasMaxLength(128) + .IsRequired(); + + e.Property(x => x.Metadata) + .HasConversion(new NullableJsonValueConverter>()) + .Metadata.SetValueComparer(JsonValueComparers.Create>()); + + e.Property(x => x.CreatedAt).HasUtcDateTimeOffsetConverter().IsRequired(); + e.Property(x => x.UpdatedAt).HasNullableUtcDateTimeOffsetConverter(); + e.Property(x => x.DeletedAt).HasNullableUtcDateTimeOffsetConverter(); + + e.HasIndex(x => new { x.Tenant, x.UserKey }); + }); + } +} \ No newline at end of file diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs index 656bc78b..3c9162ab 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs @@ -6,12 +6,16 @@ namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddUltimateAuthUsersEntityFrameworkCore(this IServiceCollection services, Action configureDb) + public static IServiceCollection AddUltimateAuthUsersEntityFrameworkCore(this IServiceCollection services, Action? configureDb = null) where TDbContext : DbContext { - services.AddDbContext(configureDb); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + if (configureDb is not null) + { + services.AddDbContext(configureDb); + } + + services.AddScoped>(); + services.AddScoped>(); + services.AddScoped>(); return services; } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserIdentifierProjections.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserIdentifierProjections.cs index 1600e974..44ecf317 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserIdentifierProjections.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserIdentifierProjections.cs @@ -4,7 +4,7 @@ namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; -internal sealed class UserIdentifierProjection +public sealed class UserIdentifierProjection { public Guid Id { get; set; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserLifecycleProjection.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserLifecycleProjection.cs index 0f33546f..07ea8cde 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserLifecycleProjection.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserLifecycleProjection.cs @@ -4,7 +4,7 @@ namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; -internal sealed class UserLifecycleProjection +public sealed class UserLifecycleProjection { public Guid Id { get; set; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserProfileProjection.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserProfileProjection.cs index 6e5f07cd..90dfed20 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserProfileProjection.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Projections/UserProfileProjection.cs @@ -3,7 +3,7 @@ namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; -internal sealed class UserProfileProjection +public sealed class UserProfileProjection { public Guid Id { get; set; } @@ -32,9 +32,7 @@ internal sealed class UserProfileProjection public Dictionary? Metadata { get; set; } public DateTimeOffset CreatedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } - public DateTimeOffset? DeletedAt { get; set; } public long Version { get; set; } diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EFCoreUserProfileStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EFCoreUserProfileStoreFactory.cs index 8ac0d8c7..e3c4147e 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EFCoreUserProfileStoreFactory.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EFCoreUserProfileStoreFactory.cs @@ -1,19 +1,20 @@ using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Users.Reference; +using Microsoft.EntityFrameworkCore; namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; -internal sealed class EfCoreUserProfileStoreFactory : IUserProfileStoreFactory +internal sealed class EfCoreUserProfileStoreFactory : IUserProfileStoreFactory where TDbContext : DbContext { - private readonly UAuthUserDbContext _db; + private readonly TDbContext _db; - public EfCoreUserProfileStoreFactory(UAuthUserDbContext db) + public EfCoreUserProfileStoreFactory(TDbContext db) { _db = db; } public IUserProfileStore Create(TenantKey tenant) { - return new EfCoreUserProfileStore(_db, new TenantContext(tenant)); + return new EfCoreUserProfileStore(_db, new TenantContext(tenant)); } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStore.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStore.cs index 5f85b529..e52bb943 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStore.cs @@ -8,22 +8,24 @@ namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; -internal sealed class EfCoreUserIdentifierStore : IUserIdentifierStore +internal sealed class EfCoreUserIdentifierStore : IUserIdentifierStore where TDbContext : DbContext { - private readonly UAuthUserDbContext _db; + private readonly TDbContext _db; private readonly TenantKey _tenant; - public EfCoreUserIdentifierStore(UAuthUserDbContext db, TenantContext tenant) + public EfCoreUserIdentifierStore(TDbContext db, TenantContext tenant) { _db = db; _tenant = tenant.Tenant; } + private DbSet DbSet => _db.Set(); + public async Task ExistsAsync(Guid key, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - return await _db.Identifiers + return await DbSet .AnyAsync(x => x.Id == key && x.Tenant == _tenant, @@ -34,7 +36,7 @@ public async Task ExistsAsync(IdentifierExistenceQuer { ct.ThrowIfCancellationRequested(); - var q = _db.Identifiers + var q = DbSet .AsNoTracking() .Where(x => x.Tenant == _tenant && @@ -82,7 +84,7 @@ public async Task ExistsAsync(IdentifierExistenceQuer { ct.ThrowIfCancellationRequested(); - var projection = await _db.Identifiers + var projection = await DbSet .AsNoTracking() .SingleOrDefaultAsync(x => x.Id == key && @@ -96,7 +98,7 @@ public async Task ExistsAsync(IdentifierExistenceQuer { ct.ThrowIfCancellationRequested(); - var projection = await _db.Identifiers + var projection = await DbSet .AsNoTracking() .SingleOrDefaultAsync( x => @@ -113,7 +115,7 @@ public async Task ExistsAsync(IdentifierExistenceQuer { ct.ThrowIfCancellationRequested(); - var projection = await _db.Identifiers + var projection = await DbSet .AsNoTracking() .SingleOrDefaultAsync(x => x.Id == id && @@ -127,7 +129,7 @@ public async Task> GetByUserAsync(UserKey userKey, { ct.ThrowIfCancellationRequested(); - var projections = await _db.Identifiers + var projections = await DbSet .AsNoTracking() .Where(x => x.Tenant == _tenant && @@ -152,7 +154,7 @@ public async Task AddAsync(UserIdentifier entity, CancellationToken ct = default if (entity.IsPrimary) { - await _db.Identifiers + await DbSet .Where(x => x.Tenant == _tenant && x.UserKey == entity.UserKey && @@ -164,7 +166,7 @@ await _db.Identifiers ct); } - _db.Identifiers.Add(projection); + DbSet.Add(projection); await _db.SaveChangesAsync(ct); await tx.CommitAsync(ct); @@ -178,7 +180,7 @@ public async Task SaveAsync(UserIdentifier entity, long expectedVersion, Cancell if (entity.IsPrimary) { - await _db.Identifiers + await DbSet .Where(x => x.Tenant == _tenant && x.UserKey == entity.UserKey && @@ -191,7 +193,7 @@ await _db.Identifiers ct); } - var existing = await _db.Identifiers + var existing = await DbSet .SingleOrDefaultAsync(x => x.Id == entity.Id && x.Tenant == _tenant, @@ -215,7 +217,7 @@ public async Task DeleteAsync(Guid key, long expectedVersion, DeleteMode mode, D { ct.ThrowIfCancellationRequested(); - var projection = await _db.Identifiers + var projection = await DbSet .SingleOrDefaultAsync(x => x.Id == key && x.Tenant == _tenant, @@ -229,7 +231,7 @@ public async Task DeleteAsync(Guid key, long expectedVersion, DeleteMode mode, D if (mode == DeleteMode.Hard) { - _db.Identifiers.Remove(projection); + DbSet.Remove(projection); } else { @@ -247,7 +249,7 @@ public async Task DeleteByUserAsync(UserKey userKey, DeleteMode mode, DateTimeOf if (mode == DeleteMode.Hard) { - await _db.Identifiers + await DbSet .Where(x => x.Tenant == _tenant && x.UserKey == userKey) @@ -256,7 +258,7 @@ await _db.Identifiers return; } - await _db.Identifiers + await DbSet .Where(x => x.Tenant == _tenant && x.UserKey == userKey && @@ -275,7 +277,7 @@ public async Task> GetByUsersAsync(IReadOnlyList(); - var projections = await _db.Identifiers + var projections = await DbSet .AsNoTracking() .Where(x => x.Tenant == _tenant && @@ -299,7 +301,7 @@ public async Task> QueryAsync(UserIdentifierQuery qu var normalized = query.Normalize(); - var baseQuery = _db.Identifiers + var baseQuery = DbSet .AsNoTracking() .Where(x => x.Tenant == _tenant && diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStoreFactory.cs index fe6cdbdf..2d343b42 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStoreFactory.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserIdentifierStoreFactory.cs @@ -4,17 +4,17 @@ namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; -internal sealed class EfCoreUserIdentifierStoreFactory : IUserIdentifierStoreFactory +internal sealed class EfCoreUserIdentifierStoreFactory : IUserIdentifierStoreFactory where TDbContext : DbContext { - private readonly UAuthUserDbContext _db; + private readonly TDbContext _db; - public EfCoreUserIdentifierStoreFactory(UAuthUserDbContext db) + public EfCoreUserIdentifierStoreFactory(TDbContext db) { _db = db; } public IUserIdentifierStore Create(TenantKey tenant) { - return new EfCoreUserIdentifierStore(_db, new TenantContext(tenant)); + return new EfCoreUserIdentifierStore(_db, new TenantContext(tenant)); } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStore.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStore.cs index eea3a8c8..63c8ef35 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStore.cs @@ -6,22 +6,24 @@ namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; -internal sealed class EfCoreUserLifecycleStore : IUserLifecycleStore +internal sealed class EfCoreUserLifecycleStore : IUserLifecycleStore where TDbContext : DbContext { - private readonly UAuthUserDbContext _db; + private readonly TDbContext _db; private readonly TenantKey _tenant; - public EfCoreUserLifecycleStore(UAuthUserDbContext db, TenantContext tenant) + public EfCoreUserLifecycleStore(TDbContext db, TenantContext tenant) { _db = db; _tenant = tenant.Tenant; } + private DbSet DbSet => _db.Set(); + public async Task GetAsync(UserLifecycleKey key, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var projection = await _db.Lifecycles + var projection = await DbSet .AsNoTracking() .SingleOrDefaultAsync( x => x.Tenant == _tenant && @@ -35,7 +37,7 @@ public async Task ExistsAsync(UserLifecycleKey key, CancellationToken ct = { ct.ThrowIfCancellationRequested(); - return await _db.Lifecycles + return await DbSet .AnyAsync( x => x.Tenant == _tenant && x.UserKey == key.UserKey, @@ -51,7 +53,7 @@ public async Task AddAsync(UserLifecycle entity, CancellationToken ct = default) var projection = entity.ToProjection(); - _db.Lifecycles.Add(projection); + DbSet.Add(projection); await _db.SaveChangesAsync(ct); } @@ -60,7 +62,7 @@ public async Task SaveAsync(UserLifecycle entity, long expectedVersion, Cancella { ct.ThrowIfCancellationRequested(); - var existing = await _db.Lifecycles + var existing = await DbSet .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == entity.UserKey, @@ -82,7 +84,7 @@ public async Task DeleteAsync(UserLifecycleKey key, long expectedVersion, Delete { ct.ThrowIfCancellationRequested(); - var projection = await _db.Lifecycles + var projection = await DbSet .SingleOrDefaultAsync( x => x.Tenant == _tenant && x.UserKey == key.UserKey, @@ -96,7 +98,7 @@ public async Task DeleteAsync(UserLifecycleKey key, long expectedVersion, Delete if (mode == DeleteMode.Hard) { - _db.Lifecycles.Remove(projection); + DbSet.Remove(projection); } else { @@ -113,7 +115,7 @@ public async Task> QueryAsync(UserLifecycleQuery quer var normalized = query.Normalize(); - var baseQuery = _db.Lifecycles + var baseQuery = DbSet .AsNoTracking() .Where(x => x.Tenant == _tenant); diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStoreFactory.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStoreFactory.cs index 9a346c7a..7e5c4d44 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStoreFactory.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserLifecycleStoreFactory.cs @@ -4,17 +4,17 @@ namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; -internal sealed class EfCoreUserLifecycleStoreFactory : IUserLifecycleStoreFactory +internal sealed class EfCoreUserLifecycleStoreFactory : IUserLifecycleStoreFactory where TDbContext : DbContext { - private readonly UAuthUserDbContext _db; + private readonly TDbContext _db; - public EfCoreUserLifecycleStoreFactory(UAuthUserDbContext db) + public EfCoreUserLifecycleStoreFactory(TDbContext db) { _db = db; } public IUserLifecycleStore Create(TenantKey tenant) { - return new EfCoreUserLifecycleStore(_db, new TenantContext(tenant)); + return new EfCoreUserLifecycleStore(_db, new TenantContext(tenant)); } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs index a3772ddb..9623dc92 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Stores/EfCoreUserProfileStore.cs @@ -7,22 +7,24 @@ namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore; -internal sealed class EfCoreUserProfileStore : IUserProfileStore +internal sealed class EfCoreUserProfileStore : IUserProfileStore where TDbContext : DbContext { - private readonly UAuthUserDbContext _db; + private readonly TDbContext _db; private readonly TenantKey _tenant; - public EfCoreUserProfileStore(UAuthUserDbContext db, TenantContext tenant) + public EfCoreUserProfileStore(TDbContext db, TenantContext tenant) { _db = db; _tenant = tenant.Tenant; } + private DbSet DbSet => _db.Set(); + public async Task GetAsync(UserProfileKey key, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); - var projection = await _db.Profiles + var projection = await DbSet .AsNoTracking() .SingleOrDefaultAsync(x => x.Tenant == _tenant && @@ -36,7 +38,7 @@ public async Task ExistsAsync(UserProfileKey key, CancellationToken ct = d { ct.ThrowIfCancellationRequested(); - return await _db.Profiles + return await DbSet .AnyAsync(x => x.Tenant == _tenant && x.UserKey == key.UserKey, @@ -52,7 +54,7 @@ public async Task AddAsync(UserProfile entity, CancellationToken ct = default) if (entity.Version != 0) throw new InvalidOperationException("New profile must have version 0."); - _db.Profiles.Add(projection); + DbSet.Add(projection); await _db.SaveChangesAsync(ct); } @@ -61,7 +63,7 @@ public async Task SaveAsync(UserProfile entity, long expectedVersion, Cancellati { ct.ThrowIfCancellationRequested(); - var existing = await _db.Profiles + var existing = await DbSet .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == entity.UserKey, @@ -83,7 +85,7 @@ public async Task DeleteAsync(UserProfileKey key, long expectedVersion, DeleteMo { ct.ThrowIfCancellationRequested(); - var projection = await _db.Profiles + var projection = await DbSet .SingleOrDefaultAsync(x => x.Tenant == _tenant && x.UserKey == key.UserKey, @@ -97,7 +99,7 @@ public async Task DeleteAsync(UserProfileKey key, long expectedVersion, DeleteMo if (mode == DeleteMode.Hard) { - _db.Profiles.Remove(projection); + DbSet.Remove(projection); } else { @@ -114,7 +116,7 @@ public async Task> QueryAsync(UserProfileQuery query, C var normalized = query.Normalize(); - var baseQuery = _db.Profiles + var baseQuery = DbSet .AsNoTracking() .Where(x => x.Tenant == _tenant); @@ -166,7 +168,7 @@ public async Task> GetByUsersAsync(IReadOnlyList x.Tenant == _tenant) .Where(x => userKeys.Contains(x.UserKey)) diff --git a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/ServiceCollectionExtensions.cs b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/ServiceCollectionExtensions.cs index 3d585691..72310f36 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/ServiceCollectionExtensions.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.InMemory/Extensions/ServiceCollectionExtensions.cs @@ -14,10 +14,6 @@ public static IServiceCollection AddUltimateAuthUsersInMemory(this IServiceColle services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.TryAddSingleton, InMemoryUserIdProvider>(); - - // Seed never try add - services.AddSingleton(); return services; } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/AuthStateSnapshotFactoryTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/AuthStateSnapshotFactoryTests.cs index aca9fa48..e2acf3aa 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/AuthStateSnapshotFactoryTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/AuthStateSnapshotFactoryTests.cs @@ -1,4 +1,6 @@ -using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Infrastructure; +using CodeBeam.UltimateAuth.Core.Contracts; using CodeBeam.UltimateAuth.Core.Domain; using CodeBeam.UltimateAuth.Core.MultiTenancy; using CodeBeam.UltimateAuth.Server.Auth; @@ -6,6 +8,7 @@ using CodeBeam.UltimateAuth.Users.Contracts; using FluentAssertions; using Moq; +using System.Text.Json; namespace CodeBeam.UltimateAuth.Tests.Unit; diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthFlowClientTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthFlowClientTests.cs new file mode 100644 index 00000000..21f57769 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthFlowClientTests.cs @@ -0,0 +1,295 @@ +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Abstractions; +using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Diagnostics; +using CodeBeam.UltimateAuth.Client.Errors; +using CodeBeam.UltimateAuth.Client.Events; +using CodeBeam.UltimateAuth.Client.Infrastructure; +using CodeBeam.UltimateAuth.Client.Options; +using CodeBeam.UltimateAuth.Client.Services; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using FluentAssertions; +using Microsoft.Extensions.Options; +using Moq; +using System.Text.Json; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class UAuthFlowClientTests +{ + private readonly Mock _mockRequest = new(); + + private UAuthFlowClient CreateClient(Mock? requestMock = null) + { + var request = requestMock ?? new Mock(); + + var events = new Mock(); + var deviceProvider = new Mock(); + var deviceIdProvider = new Mock(); + var returnUrlProvider = new Mock(); + + deviceIdProvider + .Setup(x => x.GetOrCreateAsync(It.IsAny())) + .Returns(new ValueTask( + DeviceId.Create("device-1234567890123456"))); + + returnUrlProvider + .Setup(x => x.GetCurrentUrl()) + .Returns("/home"); + + var options = Options.Create(new UAuthClientOptions + { + Endpoints = new UAuthClientEndpointOptions + { + BasePath = "/auth", + Login = "/login", + TryLogin = "/try-login" + }, + Login = new UAuthClientLoginFlowOptions + { + AllowCredentialPost = true + } + }); + + var diagnostics = new UAuthClientDiagnostics(); + + return new UAuthFlowClient( + request.Object, + events.Object, + deviceProvider.Object, + returnUrlProvider.Object, + options, + diagnostics); + } + + private static UAuthTransportResult TryLoginResponse(bool success, AuthFailureReason? reason = null) + { + return new UAuthTransportResult + { + Status = 200, + Body = JsonSerializer.SerializeToElement(new TryLoginResult + { + Success = success, + Reason = reason + }) + }; + } + + [Fact] + public async Task TryLogin_Should_Call_TryLogin_Endpoint() + { + var mock = new Mock(); + + mock.Setup(x => x.SendJsonAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(TryLoginResponse(true)); + + var client = CreateClient(mock); + await client.TryLoginAsync(new LoginRequest { Identifier = "admin", Secret = "admin" }, UAuthSubmitMode.TryOnly); + + mock.Verify(x => x.SendJsonAsync("/auth/try-login", It.IsAny()), Times.Once); + } + + [Fact] + public async Task TryLogin_Should_Return_Success() + { + var mock = new Mock(); + + mock.Setup(x => x.SendJsonAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(TryLoginResponse(true)); + + var client = CreateClient(mock); + var result = await client.TryLoginAsync(new LoginRequest { Identifier = "admin", Secret = "admin" }, UAuthSubmitMode.TryOnly); + + result.Success.Should().BeTrue(); + } + + [Fact] + public async Task TryLogin_Should_Return_Failure() + { + var mock = new Mock(); + + mock.Setup(x => x.SendJsonAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(TryLoginResponse(false, AuthFailureReason.InvalidCredentials)); + + var client = CreateClient(mock); + var result = await client.TryLoginAsync(new LoginRequest { Identifier = "admin", Secret = "wrong" }, UAuthSubmitMode.TryOnly); + + result.Success.Should().BeFalse(); + result.Reason.Should().Be(AuthFailureReason.InvalidCredentials); + } + + [Fact] + public async Task TryLogin_Should_Throw_When_Body_Null() + { + var mock = new Mock(); + + mock.Setup(x => x.SendJsonAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new UAuthTransportResult + { + Status = 200, + Body = null + }); + + var client = CreateClient(mock); + Func act = async () => await client.TryLoginAsync(new LoginRequest(), UAuthSubmitMode.TryOnly); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task TryLogin_Should_Throw_When_Invalid_Json() + { + var mock = new Mock(); + + mock.Setup(x => x.SendJsonAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new UAuthTransportResult + { + Status = 200, + Body = JsonDocument.Parse("\"invalid\"").RootElement + }); + + var client = CreateClient(mock); + Func act = async () => await client.TryLoginAsync(new LoginRequest(), UAuthSubmitMode.TryOnly); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task TryLogin_DirectCommit_Should_Navigate() + { + var mock = new Mock(); + + mock.Setup(x => x.NavigateAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(Task.CompletedTask); + + var client = CreateClient(mock); + + var request = new LoginRequest + { + Identifier = "admin", + Secret = "admin" + }; + + var result = await client.TryLoginAsync(request, UAuthSubmitMode.DirectCommit); + result.Success.Should().BeTrue(); + + mock.Verify(x => x.NavigateAsync("/auth/login", + It.Is>(d => d["Identifier"] == "admin" && d["Secret"] == "admin"), It.IsAny()), + Times.Once); + } + + [Fact] + public async Task TryLogin_TryAndCommit_Should_Call_TryAndCommit() + { + var mock = new Mock(); + + mock.Setup(x => x.TryAndCommitAsync( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new TryLoginResult { Success = true }); + + var client = CreateClient(mock); + var result = await client.TryLoginAsync(new LoginRequest(), UAuthSubmitMode.TryAndCommit); + + result.Success.Should().BeTrue(); + } + + [Fact] + public async Task TryLogin_Should_Throw_When_CredentialPost_Disabled() + { + var options = Options.Create(new UAuthClientOptions + { + Login = new UAuthClientLoginFlowOptions + { + AllowCredentialPost = false + } + }); + + var mock = new Mock(); + + var client = new UAuthFlowClient( + mock.Object, + Mock.Of(), + Mock.Of(), + Mock.Of(), + options, + new UAuthClientDiagnostics()); + + Func act = async () => await client.TryLoginAsync(new LoginRequest(), UAuthSubmitMode.TryOnly); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task Refresh_Should_Return_ReauthRequired_On_401() + { + var mock = new Mock(); + + mock.Setup(x => x.SendFormAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(new UAuthTransportResult + { + Status = 401 + }); + + var client = CreateClient(mock); + var result = await client.RefreshAsync(); + + result.IsSuccess.Should().BeFalse(); + result.Outcome.Should().Be(RefreshOutcome.ReauthRequired); + } + + [Fact] + public async Task Refresh_Should_Return_Success() + { + var mock = new Mock(); + + mock.Setup(x => x.SendFormAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(new UAuthTransportResult + { + Ok = true, + Status = 200, + RefreshOutcome = "success" + }); + + var client = CreateClient(mock); + + var result = await client.RefreshAsync(); + + result.IsSuccess.Should().BeTrue(); + result.Outcome.Should().Be(RefreshOutcome.Success); + } + + [Fact] + public async Task Validate_Should_Return_Result() + { + var mock = new Mock(); + + mock.Setup(x => x.SendFormAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(new UAuthTransportResult + { + Status = 200, + Body = JsonSerializer.SerializeToElement(new AuthValidationResult + { + State = SessionState.Active + }) + }); + + var client = CreateClient(mock); + var result = await client.ValidateAsync(); + + result.IsValid.Should().BeTrue(); + } +} \ No newline at end of file diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResultMapperTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResultMapperTests.cs new file mode 100644 index 00000000..301f457d --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Client/UAuthResultMapperTests.cs @@ -0,0 +1,136 @@ +using CodeBeam.UltimateAuth.Client.Contracts; +using CodeBeam.UltimateAuth.Client.Errors; +using CodeBeam.UltimateAuth.Client.Infrastructure; +using CodeBeam.UltimateAuth.Tests.Unit.Helpers; +using FluentAssertions; +using System.Text.Json; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public class UAuthResultMapperTests +{ + [Fact] + public void FromJson_Should_Map_Success_Response() + { + var raw = new UAuthTransportResult + { + Status = 200, + Body = JsonDocument.Parse("{\"name\":\"test\"}").RootElement + }; + + var result = UAuthResultMapper.FromJson(raw); + + result.IsSuccess.Should().BeTrue(); + result.Value!.Name.Should().Be("test"); + } + + [Fact] + public void FromJson_Should_Handle_Empty_Body() + { + var raw = new UAuthTransportResult + { + Status = 204, + Body = null + }; + + var result = UAuthResultMapper.FromJson(raw); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeNull(); + } + + [Fact] + public void FromJson_Should_Map_Problem_On_4xx() + { + var raw = new UAuthTransportResult + { + Status = 401, + Body = JsonDocument.Parse("{\"title\":\"Unauthorized\"}").RootElement + }; + + var result = UAuthResultMapper.FromJson(raw); + + result.IsSuccess.Should().BeFalse(); + result.Problem.Should().NotBeNull(); + } + + [Fact] + public void FromJson_Should_Throw_On_500() + { + var raw = new UAuthTransportResult + { + Status = 500 + }; + + Action act = () => UAuthResultMapper.FromJson(raw); + act.Should().Throw(); + } + + [Fact] + public void FromJson_Should_Throw_On_Invalid_Json() + { + var raw = new UAuthTransportResult + { + Status = 200, + Body = JsonDocument.Parse("\"invalid\"").RootElement + }; + + Action act = () => UAuthResultMapper.FromJson(raw); + act.Should().Throw(); + } + + [Fact] + public void FromJson_Should_Be_Case_Insensitive() + { + var raw = new UAuthTransportResult + { + Status = 200, + Body = JsonDocument.Parse("{\"NAME\":\"test\"}").RootElement + }; + + var result = UAuthResultMapper.FromJson(raw); + result.Value!.Name.Should().Be("test"); + } + + [Fact] + public void FromJson_Should_Handle_NullBody_With_Generic_Type() + { + var raw = new UAuthTransportResult + { + Status = 200, + Body = null + }; + + var result = UAuthResultMapper.FromJson(raw); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeNull(); + } + + [Fact] + public void FromJson_Should_Not_Throw_When_Problem_Invalid() + { + var raw = new UAuthTransportResult + { + Status = 400, + Body = JsonDocument.Parse("\"invalid\"").RootElement + }; + + var result = UAuthResultMapper.FromJson(raw); + + result.IsSuccess.Should().BeFalse(); + result.Problem.Should().BeNull(); + } + + [Fact] + public void FromJson_Should_Throw_On_Status_Zero() + { + var raw = new UAuthTransportResult + { + Status = 0 + }; + + Action act = () => UAuthResultMapper.FromJson(raw); + act.Should().Throw(); + } +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj b/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj index 073f0b56..465c2fc0 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj @@ -19,8 +19,9 @@ - + + diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs index 9144d984..35d5a069 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreAuthenticationStoreTests.cs @@ -22,7 +22,7 @@ public async Task Add_And_Get_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -50,13 +50,13 @@ public async Task Update_With_RegisterFailure_Should_Increment_Version() await using (var db1 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantContext(tenant)); await store.AddAsync(state); } await using (var db2 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantContext(tenant)); var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); var updated = existing!.RegisterFailure( @@ -69,7 +69,7 @@ public async Task Update_With_RegisterFailure_Should_Increment_Version() await using (var db3 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantContext(tenant)); var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); Assert.Equal(1, result!.SecurityVersion); @@ -84,7 +84,7 @@ public async Task Update_With_Wrong_Version_Should_Throw() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var state = AuthenticationSecurityState.CreateAccount(tenant, userKey); @@ -107,13 +107,13 @@ public async Task RegisterSuccess_Should_Clear_Failures() await using (var db1 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantContext(tenant)); await store.AddAsync(state); } await using (var db2 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantContext(tenant)); var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); var updated = existing!.RegisterSuccess(); await store.UpdateAsync(updated, expectedVersion: 1); @@ -121,7 +121,7 @@ public async Task RegisterSuccess_Should_Clear_Failures() await using (var db3 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantContext(tenant)); var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); Assert.Equal(0, result!.FailedAttempts); @@ -140,7 +140,7 @@ public async Task BeginReset_And_Consume_Should_Work() await using (var db1 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db1, new TenantContext(tenant)); await store.AddAsync(state); } @@ -148,7 +148,7 @@ public async Task BeginReset_And_Consume_Should_Work() await using (var db2 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db2, new TenantContext(tenant)); var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); var updated = existing!.BeginReset("hash", now, TimeSpan.FromMinutes(10)); await store.UpdateAsync(updated, expectedVersion: 0); @@ -156,7 +156,7 @@ public async Task BeginReset_And_Consume_Should_Work() await using (var db3 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db3, new TenantContext(tenant)); var existing = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); var consumed = existing!.ConsumeReset(DateTimeOffset.UtcNow); await store.UpdateAsync(consumed, expectedVersion: 1); @@ -164,7 +164,7 @@ public async Task BeginReset_And_Consume_Should_Work() await using (var db4 = CreateDb(connection)) { - var store = new EfCoreAuthenticationSecurityStateStore(db4, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db4, new TenantContext(tenant)); var result = await store.GetAsync(userKey, AuthenticationSecurityScope.Account, null); Assert.NotNull(result!.ResetConsumedAt); } @@ -177,7 +177,7 @@ public async Task Delete_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant)); + var store = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -201,8 +201,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() var tenant1 = TenantKeys.Single; var tenant2 = TenantKey.FromInternal("tenant-2"); - var store1 = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant1)); - var store2 = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant2)); + var store1 = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant1)); + var store2 = new EfCoreAuthenticationSecurityStateStore(db, new TenantContext(tenant2)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var state = AuthenticationSecurityState.CreateAccount(tenant1, userKey); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs index 36f0a519..23e651d1 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreCredentialStoreTests.cs @@ -24,7 +24,7 @@ public async Task Add_And_Get_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCorePasswordCredentialStore(db, new TenantContext(tenant)); + var store = new EfCorePasswordCredentialStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -52,7 +52,7 @@ public async Task Exists_Should_Return_True_When_Exists() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCorePasswordCredentialStore(db, new TenantContext(tenant)); + var store = new EfCorePasswordCredentialStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -90,13 +90,13 @@ public async Task Save_Should_Increment_Version() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); + var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); await store1.AddAsync(credential); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); + var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); var updated = existing!.ChangeSecret("new_hash", DateTimeOffset.UtcNow); await store2.SaveAsync(updated, expectedVersion: 0); @@ -104,7 +104,7 @@ public async Task Save_Should_Increment_Version() await using (var db3 = CreateDb(connection)) { - var store3 = new EfCorePasswordCredentialStore(db3, new TenantContext(tenant)); + var store3 = new EfCorePasswordCredentialStore(db3, new TenantContext(tenant)); var result = await store3.GetAsync(new CredentialKey(tenant, credential.Id)); Assert.Equal(1, result!.Version); @@ -131,13 +131,13 @@ public async Task Save_With_Wrong_Version_Should_Throw() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); + var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); await store1.AddAsync(credential); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); + var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); var updated = existing!.ChangeSecret("new_hash", DateTimeOffset.UtcNow); @@ -156,8 +156,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() var tenant1 = TenantKeys.Single; var tenant2 = TenantKey.FromInternal("tenant-2"); - var store1 = new EfCorePasswordCredentialStore(db, new TenantContext(tenant1)); - var store2 = new EfCorePasswordCredentialStore(db, new TenantContext(tenant2)); + var store1 = new EfCorePasswordCredentialStore(db, new TenantContext(tenant1)); + var store2 = new EfCorePasswordCredentialStore(db, new TenantContext(tenant2)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -183,7 +183,7 @@ public async Task Soft_Delete_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCorePasswordCredentialStore(db, new TenantContext(tenant)); + var store = new EfCorePasswordCredentialStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -229,13 +229,13 @@ public async Task Revoke_Should_Persist() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); + var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); await store1.AddAsync(credential); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); + var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); var revoked = existing!.Revoke(DateTimeOffset.UtcNow); await store2.SaveAsync(revoked, expectedVersion: 0); @@ -243,7 +243,7 @@ public async Task Revoke_Should_Persist() await using (var db3 = CreateDb(connection)) { - var store3 = new EfCorePasswordCredentialStore(db3, new TenantContext(tenant)); + var store3 = new EfCorePasswordCredentialStore(db3, new TenantContext(tenant)); var result = await store3.GetAsync(new CredentialKey(tenant, credential.Id)); Assert.True(result!.IsRevoked); @@ -269,13 +269,13 @@ public async Task ChangeSecret_Should_Update_SecurityState() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); + var store1 = new EfCorePasswordCredentialStore(db1, new TenantContext(tenant)); await store1.AddAsync(credential); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); + var store2 = new EfCorePasswordCredentialStore(db2, new TenantContext(tenant)); var existing = await store2.GetAsync(new CredentialKey(tenant, credential.Id)); var updated = existing!.ChangeSecret("new_hash", DateTimeOffset.UtcNow); @@ -284,7 +284,7 @@ public async Task ChangeSecret_Should_Update_SecurityState() await using (var db3 = CreateDb(connection)) { - var store3 = new EfCorePasswordCredentialStore(db3, new TenantContext(tenant)); + var store3 = new EfCorePasswordCredentialStore(db3, new TenantContext(tenant)); var result = await store3.GetAsync(new CredentialKey(tenant, credential.Id)); Assert.Equal("new_hash", result!.SecretHash); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs index 0332e7f6..8418e86c 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreRoleStoreTests.cs @@ -24,7 +24,7 @@ public async Task Add_And_Get_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); var role = Role.Create( null, @@ -49,7 +49,7 @@ public async Task Add_With_Duplicate_Name_Should_Throw() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); var role1 = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); var role2 = Role.Create(null, tenant, "ADMIN", null, DateTimeOffset.UtcNow); @@ -69,7 +69,7 @@ public async Task Save_Should_Increment_Version() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); var role = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); roleId = role.Id; await store.AddAsync(role); @@ -77,7 +77,7 @@ public async Task Save_Should_Increment_Version() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); var existing = await store.GetAsync(new RoleKey(tenant, roleId)); var updated = existing!.Rename("admin2", DateTimeOffset.UtcNow); await store.SaveAsync(updated, expectedVersion: 0); @@ -85,7 +85,7 @@ public async Task Save_Should_Increment_Version() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); var result = await store.GetAsync(new RoleKey(tenant, roleId)); Assert.Equal(1, result!.Version); @@ -103,7 +103,7 @@ public async Task Save_With_Wrong_Version_Should_Throw() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); var role = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); roleId = role.Id; await store.AddAsync(role); @@ -111,7 +111,7 @@ public async Task Save_With_Wrong_Version_Should_Throw() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); var existing = await store.GetAsync(new RoleKey(tenant, roleId)); var updated = existing!.Rename("admin2", DateTimeOffset.UtcNow); @@ -131,7 +131,7 @@ public async Task Rename_To_Existing_Name_Should_Throw() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); var role1 = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); var role2 = Role.Create(null, tenant, "user", null, DateTimeOffset.UtcNow); role1Id = role1.Id; @@ -142,7 +142,7 @@ public async Task Rename_To_Existing_Name_Should_Throw() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); var role = await store.GetAsync(new RoleKey(tenant, role2Id)); var updated = role!.Rename("admin", DateTimeOffset.UtcNow); @@ -160,7 +160,7 @@ public async Task Save_Should_Replace_Permissions() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); var role = Role.Create( null, @@ -176,7 +176,7 @@ public async Task Save_Should_Replace_Permissions() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); var existing = await store.GetAsync(new RoleKey(tenant, roleId)); var updated = existing!.SetPermissions( new[] @@ -189,7 +189,7 @@ public async Task Save_Should_Replace_Permissions() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); var result = await store.GetAsync(new RoleKey(tenant, roleId)); Assert.Single(result!.Permissions); @@ -207,7 +207,7 @@ public async Task Soft_Delete_Should_Work() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); var role = Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow); roleId = role.Id; await store.AddAsync(role); @@ -215,13 +215,13 @@ public async Task Soft_Delete_Should_Work() await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); await store.DeleteAsync(new RoleKey(tenant, roleId), 0, DeleteMode.Soft, DateTimeOffset.UtcNow); } await using (var db = CreateDb(connection)) { - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); var result = await store.GetAsync(new RoleKey(tenant, roleId)); Assert.NotNull(result!.DeletedAt); } @@ -234,7 +234,7 @@ public async Task Query_Should_Filter_And_Page() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreRoleStore(db, new TenantContext(tenant)); await store.AddAsync(Role.Create(null, tenant, "admin", null, DateTimeOffset.UtcNow)); await store.AddAsync(Role.Create(null, tenant, "user", null, DateTimeOffset.UtcNow)); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs index 3970a4ed..ac3d32c5 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreSessionStoreTests.cs @@ -24,7 +24,7 @@ public async Task Create_And_Get_Session_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -90,7 +90,7 @@ public async Task Session_Should_Persist_DeviceContext() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); @@ -127,7 +127,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var result = await store.GetSessionAsync(sessionId); @@ -167,7 +167,7 @@ public async Task Session_Should_Persist_Claims_And_Metadata() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); @@ -204,7 +204,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var result = await store.GetSessionAsync(sessionId); @@ -234,7 +234,7 @@ public async Task Revoke_Session_Should_Work() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); @@ -288,7 +288,7 @@ public async Task Should_Not_See_Session_From_Other_Tenant() await using (var db = CreateDb(connection)) { - var store1 = new EfCoreSessionStore(db, new TenantContext(tenant1)); + var store1 = new EfCoreSessionStore(db, new TenantContext(tenant1)); var root = UAuthSessionRoot.Create(tenant1, userKey, DateTimeOffset.UtcNow); @@ -325,7 +325,7 @@ await store1.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store2 = new EfCoreSessionStore(db, new TenantContext(tenant2)); + var store2 = new EfCoreSessionStore(db, new TenantContext(tenant2)); var result = await store2.GetSessionAsync(sessionId); @@ -343,7 +343,7 @@ public async Task ExecuteAsync_Should_Rollback_On_Error() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); await Assert.ThrowsAsync(async () => { @@ -368,7 +368,7 @@ public async Task GetSessionsByChain_Should_Return_Sessions() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); chainId = SessionChainId.New(); @@ -405,7 +405,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var sessions = await store.GetSessionsByChainAsync(chainId); Assert.Single(sessions); } @@ -423,7 +423,7 @@ public async Task ExecuteAsync_Should_Commit_Multiple_Operations() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); await store.ExecuteAsync(async ct => { @@ -460,7 +460,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var result = await store.GetSessionAsync(sessionId); @@ -480,7 +480,7 @@ public async Task ExecuteAsync_Should_Rollback_All_On_Failure() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); await Assert.ThrowsAsync(async () => { @@ -516,7 +516,7 @@ public async Task RevokeChainCascade_Should_Revoke_All_Sessions() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); chainId = SessionChainId.New(); @@ -554,7 +554,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); await store.ExecuteAsync(async ct => { @@ -564,7 +564,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var sessions = await store.GetSessionsByChainAsync(chainId); Assert.All(sessions, s => Assert.True(s.IsRevoked)); @@ -584,7 +584,7 @@ public async Task SetActiveSession_Should_Work() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); @@ -612,7 +612,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var active = await store.GetActiveSessionIdAsync(chainId); Assert.Equal(sessionId, active); @@ -648,7 +648,7 @@ public async Task SaveSession_Should_Increment_Version() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); chainId = SessionChainId.New(); @@ -686,7 +686,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); await store.ExecuteAsync(async ct => { @@ -699,7 +699,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var result = await store.GetSessionAsync(sessionId); Assert.Equal(1, result!.Version); @@ -719,7 +719,7 @@ public async Task SaveSession_With_Wrong_Version_Should_Throw() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); chainId = SessionChainId.New(); @@ -757,7 +757,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); await Assert.ThrowsAsync(async () => { @@ -784,7 +784,7 @@ public async Task SaveChain_Should_Increment_Version() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var root = UAuthSessionRoot.Create(tenant, userKey, DateTimeOffset.UtcNow); chainId = SessionChainId.New(); @@ -809,7 +809,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); await store.ExecuteAsync(async ct => { @@ -822,7 +822,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var result = await store.GetChainAsync(chainId); Assert.Equal(1, result!.Version); @@ -839,7 +839,7 @@ public async Task SaveRoot_Should_Increment_Version() await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var root = UAuthSessionRoot.Create( tenant, @@ -854,7 +854,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); await store.ExecuteAsync(async ct => { @@ -867,7 +867,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreSessionStore(db, new TenantContext(tenant)); + var store = new EfCoreSessionStore(db, new TenantContext(tenant)); var result = await store.GetRootByUserAsync(userKey); Assert.Equal(1, result!.Version); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs index 58e608a9..c7bd8179 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreTokenStoreTests.cs @@ -22,7 +22,7 @@ public async Task Store_And_Find_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); AuthSessionId.TryCreate(ValidRaw, out var sessionId); var token = RefreshToken.Create( @@ -58,7 +58,7 @@ public async Task Revoke_Should_Set_RevokedAt() await using (var db = CreateDb(connection)) { - var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); var token = RefreshToken.Create( TokenId.From(Guid.NewGuid()), @@ -79,7 +79,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); await store.ExecuteAsync(async ct => { @@ -89,7 +89,7 @@ await store.ExecuteAsync(async ct => await using (var db = CreateDb(connection)) { - var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); var result = await store.FindByHashAsync(tokenHash); Assert.NotNull(result!.RevokedAt); @@ -103,7 +103,7 @@ public async Task Store_Outside_Transaction_Should_Throw() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); + var store = new EfCoreRefreshTokenStore(db, new TenantContext(tenant)); AuthSessionId.TryCreate(ValidRaw, out var sessionId); var token = RefreshToken.Create( diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs index 751f24f2..4fd806af 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserIdentifierStoreTests.cs @@ -23,7 +23,7 @@ public async Task Add_And_Get_Should_Work() using var connection = CreateOpenConnection(); await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserIdentifierStore(db, new TenantContext(tenant)); + var store = new EfCoreUserIdentifierStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var identifier = UserIdentifier.Create( @@ -49,7 +49,7 @@ public async Task Exists_Should_Return_True_When_Exists() using var connection = CreateOpenConnection(); await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserIdentifierStore(db, new TenantContext(tenant)); + var store = new EfCoreUserIdentifierStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var identifier = UserIdentifier.Create( @@ -77,7 +77,7 @@ public async Task Save_With_Wrong_Version_Should_Throw() var userKey = UserKey.FromGuid(Guid.NewGuid()); await using var db1 = CreateDb(connection); - var store1 = new EfCoreUserIdentifierStore(db1, new TenantContext(tenant)); + var store1 = new EfCoreUserIdentifierStore(db1, new TenantContext(tenant)); var identifier = UserIdentifier.Create( Guid.NewGuid(), @@ -92,7 +92,7 @@ public async Task Save_With_Wrong_Version_Should_Throw() await store1.AddAsync(identifier); await using var db2 = CreateDb(connection); - var store2 = new EfCoreUserIdentifierStore(db2, new TenantContext(tenant)); + var store2 = new EfCoreUserIdentifierStore(db2, new TenantContext(tenant)); var updated = identifier.SetPrimary(DateTimeOffset.UtcNow); @@ -120,13 +120,13 @@ public async Task Save_Should_Increment_Version() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCoreUserIdentifierStore(db1, new TenantContext(tenant)); + var store1 = new EfCoreUserIdentifierStore(db1, new TenantContext(tenant)); await store1.AddAsync(identifier); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCoreUserIdentifierStore(db2, new TenantContext(tenant)); + var store2 = new EfCoreUserIdentifierStore(db2, new TenantContext(tenant)); var existing = await store2.GetAsync(identifier.Id); var updated = existing!.SetPrimary(DateTimeOffset.UtcNow); await store2.SaveAsync(updated, expectedVersion: 0); @@ -134,7 +134,7 @@ public async Task Save_Should_Increment_Version() await using (var db3 = CreateDb(connection)) { - var store3 = new EfCoreUserIdentifierStore(db3, new TenantContext(tenant)); + var store3 = new EfCoreUserIdentifierStore(db3, new TenantContext(tenant)); var result = await store3.GetAsync(identifier.Id); Assert.Equal(1, result!.Version); } @@ -147,8 +147,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() await using var db = CreateDb(connection); var tenant1 = TenantKeys.Single; var tenant2 = TenantKey.FromInternal("tenant-2"); - var store1 = new EfCoreUserIdentifierStore(db, new TenantContext(tenant1)); - var store2 = new EfCoreUserIdentifierStore(db, new TenantContext(tenant2)); + var store1 = new EfCoreUserIdentifierStore(db, new TenantContext(tenant1)); + var store2 = new EfCoreUserIdentifierStore(db, new TenantContext(tenant2)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -174,7 +174,7 @@ public async Task Soft_Delete_Should_Work() using var connection = CreateOpenConnection(); await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserIdentifierStore(db, new TenantContext(tenant)); + var store = new EfCoreUserIdentifierStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var identifier = UserIdentifier.Create( diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs index 4c08b7b3..0c7c0333 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserLifecycleStoreTests.cs @@ -23,7 +23,7 @@ public async Task Add_And_Get_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserLifecycleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserLifecycleStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -48,7 +48,7 @@ public async Task Exists_Should_Return_True_When_Exists() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserLifecycleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserLifecycleStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -78,13 +78,13 @@ public async Task Save_Should_Increment_Version() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCoreUserLifecycleStore(db1, new TenantContext(tenant)); + var store1 = new EfCoreUserLifecycleStore(db1, new TenantContext(tenant)); await store1.AddAsync(lifecycle); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCoreUserLifecycleStore(db2, new TenantContext(tenant)); + var store2 = new EfCoreUserLifecycleStore(db2, new TenantContext(tenant)); var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey)); var updated = existing!.ChangeStatus(DateTimeOffset.UtcNow, UserStatus.Suspended); await store2.SaveAsync(updated, expectedVersion: 0); @@ -92,7 +92,7 @@ public async Task Save_Should_Increment_Version() await using (var db3 = CreateDb(connection)) { - var store3 = new EfCoreUserLifecycleStore(db3, new TenantContext(tenant)); + var store3 = new EfCoreUserLifecycleStore(db3, new TenantContext(tenant)); var result = await store3.GetAsync(new UserLifecycleKey(tenant, userKey)); Assert.Equal(1, result!.Version); @@ -115,13 +115,13 @@ public async Task Save_With_Wrong_Version_Should_Throw() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCoreUserLifecycleStore(db1, new TenantContext(tenant)); + var store1 = new EfCoreUserLifecycleStore(db1, new TenantContext(tenant)); await store1.AddAsync(lifecycle); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCoreUserLifecycleStore(db2, new TenantContext(tenant)); + var store2 = new EfCoreUserLifecycleStore(db2, new TenantContext(tenant)); var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey)); var updated = existing!.ChangeStatus(DateTimeOffset.UtcNow, UserStatus.Suspended); @@ -139,8 +139,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() var tenant1 = TenantKeys.Single; var tenant2 = TenantKey.FromInternal("tenant-2"); - var store1 = new EfCoreUserLifecycleStore(db, new TenantContext(tenant1)); - var store2 = new EfCoreUserLifecycleStore(db, new TenantContext(tenant2)); + var store1 = new EfCoreUserLifecycleStore(db, new TenantContext(tenant1)); + var store2 = new EfCoreUserLifecycleStore(db, new TenantContext(tenant2)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -163,7 +163,7 @@ public async Task Soft_Delete_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserLifecycleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserLifecycleStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -201,13 +201,13 @@ public async Task Delete_Should_Increment_SecurityVersion() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCoreUserLifecycleStore(db1, new TenantContext(tenant)); + var store1 = new EfCoreUserLifecycleStore(db1, new TenantContext(tenant)); await store1.AddAsync(lifecycle); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCoreUserLifecycleStore(db2, new TenantContext(tenant)); + var store2 = new EfCoreUserLifecycleStore(db2, new TenantContext(tenant)); var existing = await store2.GetAsync(new UserLifecycleKey(tenant, userKey)); var deleted = existing!.MarkDeleted(DateTimeOffset.UtcNow); @@ -217,7 +217,7 @@ public async Task Delete_Should_Increment_SecurityVersion() await using (var db3 = CreateDb(connection)) { - var store3 = new EfCoreUserLifecycleStore(db3, new TenantContext(tenant)); + var store3 = new EfCoreUserLifecycleStore(db3, new TenantContext(tenant)); var result = await store3.GetAsync(new UserLifecycleKey(tenant, userKey)); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs index db39136f..87d84078 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserProfileStoreTests.cs @@ -23,7 +23,7 @@ public async Task Add_And_Get_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -51,7 +51,7 @@ public async Task Exists_Should_Return_True_When_Exists() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -91,13 +91,13 @@ public async Task Save_Should_Increment_Version() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCoreUserProfileStore(db1, new TenantContext(tenant)); + var store1 = new EfCoreUserProfileStore(db1, new TenantContext(tenant)); await store1.AddAsync(profile); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCoreUserProfileStore(db2, new TenantContext(tenant)); + var store2 = new EfCoreUserProfileStore(db2, new TenantContext(tenant)); var existing = await store2.GetAsync(new UserProfileKey(tenant, userKey)); var updated = existing!.UpdateName(existing.FirstName, existing.LastName, "new", DateTimeOffset.UtcNow); await store2.SaveAsync(updated, expectedVersion: 0); @@ -105,7 +105,7 @@ public async Task Save_Should_Increment_Version() await using (var db3 = CreateDb(connection)) { - var store3 = new EfCoreUserProfileStore(db3, new TenantContext(tenant)); + var store3 = new EfCoreUserProfileStore(db3, new TenantContext(tenant)); var result = await store3.GetAsync(new UserProfileKey(tenant, userKey)); Assert.Equal(1, result!.Version); @@ -133,13 +133,13 @@ public async Task Save_With_Wrong_Version_Should_Throw() await using (var db1 = CreateDb(connection)) { - var store1 = new EfCoreUserProfileStore(db1, new TenantContext(tenant)); + var store1 = new EfCoreUserProfileStore(db1, new TenantContext(tenant)); await store1.AddAsync(profile); } await using (var db2 = CreateDb(connection)) { - var store2 = new EfCoreUserProfileStore(db2, new TenantContext(tenant)); + var store2 = new EfCoreUserProfileStore(db2, new TenantContext(tenant)); var existing = await store2.GetAsync(new UserProfileKey(tenant, userKey)); var updated = existing!.UpdateName(existing.FirstName, existing.LastName, "new", DateTimeOffset.UtcNow); @@ -157,8 +157,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() var tenant1 = TenantKeys.Single; var tenant2 = TenantKey.FromInternal("tenant-2"); - var store1 = new EfCoreUserProfileStore(db, new TenantContext(tenant1)); - var store2 = new EfCoreUserProfileStore(db, new TenantContext(tenant2)); + var store1 = new EfCoreUserProfileStore(db, new TenantContext(tenant1)); + var store2 = new EfCoreUserProfileStore(db, new TenantContext(tenant2)); var userKey = UserKey.FromGuid(Guid.NewGuid()); @@ -185,7 +185,7 @@ public async Task Soft_Delete_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); + var store = new EfCoreUserProfileStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs index f1b5f3b6..e1062e34 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/EntityFrameworkCore/EfCoreUserRoleStoreTests.cs @@ -22,7 +22,7 @@ public async Task Assign_And_GetAssignments_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); @@ -41,7 +41,7 @@ public async Task Assign_Duplicate_Should_Throw() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); @@ -58,7 +58,7 @@ public async Task Remove_Should_Work() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); @@ -77,7 +77,7 @@ public async Task Remove_NonExisting_Should_Not_Throw() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); @@ -92,7 +92,7 @@ public async Task CountAssignments_Should_Return_Correct_Count() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); var roleId = RoleId.New(); @@ -111,7 +111,7 @@ public async Task RemoveAssignmentsByRole_Should_Remove_All() await using var db = CreateDb(connection); var tenant = TenantKeys.Single; - var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); + var store = new EfCoreUserRoleStore(db, new TenantContext(tenant)); var roleId = RoleId.New(); @@ -137,8 +137,8 @@ public async Task Should_Not_See_Data_From_Other_Tenant() var tenant1 = TenantKeys.Single; var tenant2 = TenantKey.FromInternal("tenant-2"); - var store1 = new EfCoreUserRoleStore(db, new TenantContext(tenant1)); - var store2 = new EfCoreUserRoleStore(db, new TenantContext(tenant2)); + var store1 = new EfCoreUserRoleStore(db, new TenantContext(tenant1)); + var store2 = new EfCoreUserRoleStore(db, new TenantContext(tenant2)); var userKey = UserKey.FromGuid(Guid.NewGuid()); var roleId = RoleId.New(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestAuthRuntime.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestAuthRuntime.cs index 38a62c52..a37eb1d7 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestAuthRuntime.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestAuthRuntime.cs @@ -8,6 +8,7 @@ using CodeBeam.UltimateAuth.Core.Options; using CodeBeam.UltimateAuth.Credentials.Reference; using CodeBeam.UltimateAuth.InMemory; +using CodeBeam.UltimateAuth.Sample.Seed.Extensions; using CodeBeam.UltimateAuth.Server.Auth; using CodeBeam.UltimateAuth.Server.Extensions; using CodeBeam.UltimateAuth.Server.Flows; @@ -36,6 +37,8 @@ public TestAuthRuntime(Action? configureServer = null, Actio configureServer?.Invoke(options); }); + services.AddUltimateAuthSampleSeed(); + services.AddSingleton(); // InMemory plugins services.AddUltimateAuthInMemory(); diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestDto.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestDto.cs new file mode 100644 index 00000000..ab94f520 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestDto.cs @@ -0,0 +1,6 @@ +namespace CodeBeam.UltimateAuth.Tests.Unit.Helpers; + +public sealed class TestDto +{ + public string? Name { get; set; } +}