diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..22af469
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,3 @@
+# Local-only SQL Server password used by docker compose and the Aspire AppHost.
+# This file is optional; docker-compose.yml already has the same default.
+MSSQL_SA_PASSWORD=LocalDev_Sql#2026
diff --git a/.gitignore b/.gitignore
index a4fe18b..413f6fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -398,3 +398,6 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
+
+# Local docker compose overrides
+.env
diff --git a/Authorization.API.AppHost/Authorization.API.AppHost.csproj b/Authorization.API.AppHost/Authorization.API.AppHost.csproj
index b384b72..6c16c92 100644
--- a/Authorization.API.AppHost/Authorization.API.AppHost.csproj
+++ b/Authorization.API.AppHost/Authorization.API.AppHost.csproj
@@ -13,6 +13,7 @@
+
diff --git a/Authorization.API.AppHost/Program.cs b/Authorization.API.AppHost/Program.cs
index 3318e9c..7fba30b 100644
--- a/Authorization.API.AppHost/Program.cs
+++ b/Authorization.API.AppHost/Program.cs
@@ -1,5 +1,16 @@
var builder = DistributedApplication.CreateBuilder(args);
-builder.AddProject("authorization-api");
+// Fixed local password so a persisted data volume keeps working across restarts.
+// Override with the sql-password parameter / user secret if needed.
+var sqlPassword = builder.AddParameter("sql-password", "LocalDev_Sql#2026", secret: true);
+
+var database = builder.AddSqlServer("sql", password: sqlPassword)
+ .WithDataVolume("authorization-sql-data")
+ .AddDatabase("DefaultConnection");
+
+builder.AddProject("authorization-api")
+ .WithReference(database)
+ .WaitFor(database)
+ .WithExternalHttpEndpoints();
builder.Build().Run();
diff --git a/Authorization.API.AppHost/appsettings.Development.json b/Authorization.API.AppHost/appsettings.Development.json
index 0c208ae..3ce93ad 100644
--- a/Authorization.API.AppHost/appsettings.Development.json
+++ b/Authorization.API.AppHost/appsettings.Development.json
@@ -4,5 +4,8 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
+ },
+ "Parameters": {
+ "sql-password": "LocalDev_Sql#2026"
}
}
diff --git a/Authorization.API.Tests/Authorization.API.Tests.csproj b/Authorization.API.Tests/Authorization.API.Tests.csproj
index 820c448..7d39e2c 100644
--- a/Authorization.API.Tests/Authorization.API.Tests.csproj
+++ b/Authorization.API.Tests/Authorization.API.Tests.csproj
@@ -9,6 +9,7 @@
+
diff --git a/Authorization.API.Tests/DatabaseInitializerTests.cs b/Authorization.API.Tests/DatabaseInitializerTests.cs
new file mode 100644
index 0000000..aee44ef
--- /dev/null
+++ b/Authorization.API.Tests/DatabaseInitializerTests.cs
@@ -0,0 +1,47 @@
+using Authorization.API.Context;
+using Authorization.API.Data;
+using Authorization.API.Models;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Authorization.API.Tests;
+
+public class DatabaseInitializerTests
+{
+ [Fact]
+ public async Task SeedAsync_CreatesDemoUsersAndClient()
+ {
+ await using var connection = new SqliteConnection("DataSource=:memory:");
+ await connection.OpenAsync();
+
+ var services = new ServiceCollection();
+ services.AddLogging();
+ services.AddDbContext(options => options.UseSqlite(connection));
+ services.AddIdentity()
+ .AddEntityFrameworkStores()
+ .AddDefaultTokenProviders();
+
+ await using var provider = services.BuildServiceProvider();
+ await using var scope = provider.CreateAsyncScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+ await db.Database.EnsureCreatedAsync();
+
+ var options = new SeedOptions { Enabled = true };
+ await DatabaseInitializer.SeedAsync(scope.ServiceProvider, options, NullLogger.Instance);
+
+ var userManager = scope.ServiceProvider.GetRequiredService>();
+ var admin = await userManager.FindByEmailAsync(options.AdminEmail);
+ var demo = await userManager.FindByEmailAsync(options.DemoUserEmail);
+ var client = await db.Clients.SingleAsync(c => c.ClientId == options.DemoClientId);
+
+ Assert.NotNull(admin);
+ Assert.NotNull(demo);
+ Assert.True(await userManager.IsInRoleAsync(admin!, "Administrator"));
+ Assert.Equal(options.DemoClientSecret, client.ClientSecret);
+ Assert.Contains("api", client.AllowedScopes);
+ Assert.True(await db.ApiScopes.AnyAsync(s => s.Name == "openid"));
+ }
+}
diff --git a/Authorization.API/Authorization.API.csproj b/Authorization.API/Authorization.API.csproj
index fabd9a6..feb5043 100644
--- a/Authorization.API/Authorization.API.csproj
+++ b/Authorization.API/Authorization.API.csproj
@@ -12,6 +12,10 @@
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
diff --git a/Authorization.API/Data/DatabaseInitializer.cs b/Authorization.API/Data/DatabaseInitializer.cs
new file mode 100644
index 0000000..e97a998
--- /dev/null
+++ b/Authorization.API/Data/DatabaseInitializer.cs
@@ -0,0 +1,161 @@
+using Authorization.API.Context;
+using Authorization.API.Models;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Options;
+
+namespace Authorization.API.Data;
+
+public static class DatabaseInitializer
+{
+ public static async Task InitializeAsync(IServiceProvider services, CancellationToken cancellationToken = default)
+ {
+ await using var scope = services.CreateAsyncScope();
+ var provider = scope.ServiceProvider;
+ var config = provider.GetRequiredService();
+ var logger = provider.GetRequiredService().CreateLogger(nameof(DatabaseInitializer));
+
+ var migrateOnStartup = config.GetValue("Database:MigrateOnStartup", false);
+ if (!migrateOnStartup)
+ {
+ return;
+ }
+
+ var db = provider.GetRequiredService();
+ await WaitAndMigrateAsync(db, logger, cancellationToken);
+
+ var seedOptions = provider.GetRequiredService>().Value;
+ if (!seedOptions.Enabled)
+ {
+ return;
+ }
+
+ await SeedAsync(provider, seedOptions, logger, cancellationToken);
+ }
+
+ public static async Task WaitAndMigrateAsync(
+ ApplicationDbContext db,
+ ILogger logger,
+ CancellationToken cancellationToken = default)
+ {
+ var delaysSeconds = new[] { 2, 3, 5, 8, 8, 8, 8, 8, 8, 8 };
+
+ for (var attempt = 0; attempt < delaysSeconds.Length; attempt++)
+ {
+ try
+ {
+ if (db.Database.IsRelational())
+ {
+ if (db.Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite")
+ {
+ await db.Database.EnsureCreatedAsync(cancellationToken);
+ }
+ else
+ {
+ await db.Database.MigrateAsync(cancellationToken);
+ }
+ }
+ else
+ {
+ await db.Database.EnsureCreatedAsync(cancellationToken);
+ }
+
+ logger.LogInformation("Database is ready.");
+ return;
+ }
+ catch (Exception ex) when (attempt < delaysSeconds.Length - 1 && !cancellationToken.IsCancellationRequested)
+ {
+ logger.LogWarning(
+ ex,
+ "Database is not ready (attempt {Attempt}/{Total}). Retrying in {Delay}s.",
+ attempt + 1,
+ delaysSeconds.Length,
+ delaysSeconds[attempt]);
+ await Task.Delay(TimeSpan.FromSeconds(delaysSeconds[attempt]), cancellationToken);
+ }
+ }
+ }
+
+ public static async Task SeedAsync(
+ IServiceProvider services,
+ SeedOptions options,
+ ILogger logger,
+ CancellationToken cancellationToken = default)
+ {
+ var roleManager = services.GetRequiredService>();
+ var userManager = services.GetRequiredService>();
+ var db = services.GetRequiredService();
+
+ foreach (var roleName in new[] { "Administrator", "User" })
+ {
+ if (!await roleManager.RoleExistsAsync(roleName))
+ {
+ await roleManager.CreateAsync(new ApplicationRole(roleName));
+ }
+ }
+
+ await EnsureUserAsync(userManager, options.AdminEmail, options.AdminPassword, "Administrator");
+ await EnsureUserAsync(userManager, options.DemoUserEmail, options.DemoUserPassword, "User");
+
+ foreach (var (name, description) in new[]
+ {
+ ("openid", "OpenID"),
+ ("profile", "User profile"),
+ ("email", "Email address"),
+ ("api", "API access")
+ })
+ {
+ if (!await db.ApiScopes.AnyAsync(s => s.Name == name, cancellationToken))
+ {
+ db.ApiScopes.Add(new ApiScope { Name = name, Description = description });
+ }
+ }
+
+ if (!await db.Clients.AnyAsync(c => c.ClientId == options.DemoClientId, cancellationToken))
+ {
+ db.Clients.Add(new Client
+ {
+ ClientId = options.DemoClientId,
+ Description = "Local demo confidential client",
+ ClientSecret = options.DemoClientSecret,
+ RedirectUri = options.DemoRedirectUri,
+ RequirePkce = true,
+ AllowRefreshToken = true,
+ AllowedScopes = ["openid", "profile", "email", "api"]
+ });
+ }
+
+ await db.SaveChangesAsync(cancellationToken);
+ logger.LogInformation("Development seed data is in place.");
+ }
+
+ private static async Task EnsureUserAsync(
+ UserManager userManager,
+ string email,
+ string password,
+ string role)
+ {
+ var user = await userManager.FindByEmailAsync(email);
+ if (user == null)
+ {
+ user = new ApplicationUser
+ {
+ UserName = email,
+ Email = email,
+ EmailConfirmed = true
+ };
+
+ var result = await userManager.CreateAsync(user, password);
+ if (!result.Succeeded)
+ {
+ throw new InvalidOperationException(
+ $"Failed to seed user {email}: {string.Join(", ", result.Errors.Select(e => e.Description))}");
+ }
+ }
+
+ if (!await userManager.IsInRoleAsync(user, role))
+ {
+ await userManager.AddToRoleAsync(user, role);
+ }
+ }
+}
diff --git a/Authorization.API/Data/SeedOptions.cs b/Authorization.API/Data/SeedOptions.cs
new file mode 100644
index 0000000..e4ba98d
--- /dev/null
+++ b/Authorization.API/Data/SeedOptions.cs
@@ -0,0 +1,22 @@
+namespace Authorization.API.Data;
+
+public class SeedOptions
+{
+ public const string SectionName = "Seed";
+
+ public bool Enabled { get; set; }
+
+ public string AdminEmail { get; set; } = "admin@localhost";
+
+ public string AdminPassword { get; set; } = "Admin123!";
+
+ public string DemoUserEmail { get; set; } = "demo@example.com";
+
+ public string DemoUserPassword { get; set; } = "Password1!";
+
+ public string DemoClientId { get; set; } = "demo-client";
+
+ public string DemoClientSecret { get; set; } = "demo-secret";
+
+ public string DemoRedirectUri { get; set; } = "http://localhost:3000/callback";
+}
diff --git a/Authorization.API/Migrations/20260826141731_InitialCreate.Designer.cs b/Authorization.API/Migrations/20260826141731_InitialCreate.Designer.cs
new file mode 100644
index 0000000..886729f
--- /dev/null
+++ b/Authorization.API/Migrations/20260826141731_InitialCreate.Designer.cs
@@ -0,0 +1,486 @@
+//
+using System;
+using Authorization.API.Context;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Authorization.API.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20260826141731_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Authorization.API.Models.ApiResource", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("ApiResources");
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.ApiScope", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ApiResourceId")
+ .HasColumnType("int");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ApiResourceId");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("ApiScopes");
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.ApplicationRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex")
+ .HasFilter("[NormalizedName] IS NOT NULL");
+
+ b.ToTable("Roles", (string)null);
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("int");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("FullName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("bit");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex")
+ .HasFilter("[NormalizedUserName] IS NOT NULL");
+
+ b.ToTable("Users", (string)null);
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.AuthorizationCode", b =>
+ {
+ b.Property("Code")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ClientId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("CodeChallenge")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CodeChallengeMethod")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime2");
+
+ b.Property("IsUsed")
+ .HasColumnType("bit");
+
+ b.Property("RedirectUri")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Scopes")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Subject")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Code");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("ClientId", "UserId");
+
+ b.ToTable("AuthorizationCodes");
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.Client", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AllowRefreshToken")
+ .HasColumnType("bit");
+
+ b.PrimitiveCollection("AllowedScopes")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClientId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ClientSecret")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PostLogoutRedirectUri")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RedirectUri")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RequirePkce")
+ .HasColumnType("bit");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId")
+ .IsUnique();
+
+ b.ToTable("Clients");
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.RefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClientId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("Expiry")
+ .HasColumnType("datetime2");
+
+ b.Property("IsRevoked")
+ .HasColumnType("bit");
+
+ b.Property("Token")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId");
+
+ b.HasIndex("Token")
+ .IsUnique();
+
+ b.HasIndex("UserId");
+
+ b.ToTable("RefreshTokens");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderKey")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("RoleId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Value")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.ApiScope", b =>
+ {
+ b.HasOne("Authorization.API.Models.ApiResource", null)
+ .WithMany("Scopes")
+ .HasForeignKey("ApiResourceId");
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.RefreshToken", b =>
+ {
+ b.HasOne("Authorization.API.Models.Client", "Client")
+ .WithMany()
+ .HasForeignKey("ClientId")
+ .HasPrincipalKey("ClientId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("Authorization.API.Models.ApplicationUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("Client");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Authorization.API.Models.ApplicationRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("Authorization.API.Models.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("Authorization.API.Models.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Authorization.API.Models.ApplicationRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Authorization.API.Models.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("Authorization.API.Models.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.ApiResource", b =>
+ {
+ b.Navigation("Scopes");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Authorization.API/Migrations/20260826141731_InitialCreate.cs b/Authorization.API/Migrations/20260826141731_InitialCreate.cs
new file mode 100644
index 0000000..62309a9
--- /dev/null
+++ b/Authorization.API/Migrations/20260826141731_InitialCreate.cs
@@ -0,0 +1,389 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Authorization.API.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "ApiResources",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Name = table.Column(type: "nvarchar(max)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ApiResources", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AuthorizationCodes",
+ columns: table => new
+ {
+ Code = table.Column(type: "nvarchar(450)", nullable: false),
+ ClientId = table.Column(type: "nvarchar(450)", nullable: false),
+ UserId = table.Column(type: "nvarchar(450)", nullable: true),
+ Subject = table.Column(type: "nvarchar(max)", nullable: false),
+ CreatedAt = table.Column(type: "datetime2", nullable: false),
+ ExpiresAt = table.Column(type: "datetime2", nullable: false),
+ IsUsed = table.Column(type: "bit", nullable: false),
+ RedirectUri = table.Column(type: "nvarchar(max)", nullable: true),
+ CodeChallenge = table.Column(type: "nvarchar(max)", nullable: true),
+ CodeChallengeMethod = table.Column(type: "nvarchar(max)", nullable: true),
+ Scopes = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AuthorizationCodes", x => x.Code);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Clients",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ ClientId = table.Column(type: "nvarchar(450)", nullable: false),
+ Description = table.Column(type: "nvarchar(max)", nullable: true),
+ ClientSecret = table.Column(type: "nvarchar(max)", nullable: false),
+ RedirectUri = table.Column(type: "nvarchar(max)", nullable: true),
+ PostLogoutRedirectUri = table.Column(type: "nvarchar(max)", nullable: true),
+ RequirePkce = table.Column(type: "bit", nullable: false),
+ AllowRefreshToken = table.Column(type: "bit", nullable: false),
+ AllowedScopes = table.Column(type: "nvarchar(max)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Clients", x => x.Id);
+ table.UniqueConstraint("AK_Clients_ClientId", x => x.ClientId);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Roles",
+ columns: table => new
+ {
+ Id = table.Column(type: "nvarchar(450)", nullable: false),
+ Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Roles", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Users",
+ columns: table => new
+ {
+ Id = table.Column(type: "nvarchar(450)", nullable: false),
+ FullName = table.Column(type: "nvarchar(max)", nullable: true),
+ UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ EmailConfirmed = table.Column(type: "bit", nullable: false),
+ PasswordHash = table.Column(type: "nvarchar(max)", nullable: true),
+ SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true),
+ ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true),
+ PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true),
+ PhoneNumberConfirmed = table.Column(type: "bit", nullable: false),
+ TwoFactorEnabled = table.Column(type: "bit", nullable: false),
+ LockoutEnd = table.Column(type: "datetimeoffset", nullable: true),
+ LockoutEnabled = table.Column(type: "bit", nullable: false),
+ AccessFailedCount = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Users", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "ApiScopes",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Name = table.Column(type: "nvarchar(450)", nullable: false),
+ Description = table.Column(type: "nvarchar(max)", nullable: true),
+ ApiResourceId = table.Column(type: "int", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ApiScopes", x => x.Id);
+ table.ForeignKey(
+ name: "FK_ApiScopes_ApiResources_ApiResourceId",
+ column: x => x.ApiResourceId,
+ principalTable: "ApiResources",
+ principalColumn: "Id");
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetRoleClaims",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ RoleId = table.Column(type: "nvarchar(450)", nullable: false),
+ ClaimType = table.Column(type: "nvarchar(max)", nullable: true),
+ ClaimValue = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
+ table.ForeignKey(
+ name: "FK_AspNetRoleClaims_Roles_RoleId",
+ column: x => x.RoleId,
+ principalTable: "Roles",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserClaims",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ UserId = table.Column(type: "nvarchar(450)", nullable: false),
+ ClaimType = table.Column(type: "nvarchar(max)", nullable: true),
+ ClaimValue = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
+ table.ForeignKey(
+ name: "FK_AspNetUserClaims_Users_UserId",
+ column: x => x.UserId,
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserLogins",
+ columns: table => new
+ {
+ LoginProvider = table.Column(type: "nvarchar(450)", nullable: false),
+ ProviderKey = table.Column(type: "nvarchar(450)", nullable: false),
+ ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true),
+ UserId = table.Column(type: "nvarchar(450)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
+ table.ForeignKey(
+ name: "FK_AspNetUserLogins_Users_UserId",
+ column: x => x.UserId,
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserRoles",
+ columns: table => new
+ {
+ UserId = table.Column(type: "nvarchar(450)", nullable: false),
+ RoleId = table.Column(type: "nvarchar(450)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
+ table.ForeignKey(
+ name: "FK_AspNetUserRoles_Roles_RoleId",
+ column: x => x.RoleId,
+ principalTable: "Roles",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_AspNetUserRoles_Users_UserId",
+ column: x => x.UserId,
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserTokens",
+ columns: table => new
+ {
+ UserId = table.Column(type: "nvarchar(450)", nullable: false),
+ LoginProvider = table.Column(type: "nvarchar(450)", nullable: false),
+ Name = table.Column(type: "nvarchar(450)", nullable: false),
+ Value = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
+ table.ForeignKey(
+ name: "FK_AspNetUserTokens_Users_UserId",
+ column: x => x.UserId,
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "RefreshTokens",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Token = table.Column(type: "nvarchar(450)", nullable: false),
+ UserId = table.Column(type: "nvarchar(450)", nullable: true),
+ ClientId = table.Column(type: "nvarchar(450)", nullable: true),
+ Expiry = table.Column(type: "datetime2", nullable: false),
+ IsRevoked = table.Column(type: "bit", nullable: false),
+ Created = table.Column(type: "datetime2", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_RefreshTokens", x => x.Id);
+ table.ForeignKey(
+ name: "FK_RefreshTokens_Clients_ClientId",
+ column: x => x.ClientId,
+ principalTable: "Clients",
+ principalColumn: "ClientId",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_RefreshTokens_Users_UserId",
+ column: x => x.UserId,
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ApiScopes_ApiResourceId",
+ table: "ApiScopes",
+ column: "ApiResourceId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ApiScopes_Name",
+ table: "ApiScopes",
+ column: "Name",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetRoleClaims_RoleId",
+ table: "AspNetRoleClaims",
+ column: "RoleId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserClaims_UserId",
+ table: "AspNetUserClaims",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserLogins_UserId",
+ table: "AspNetUserLogins",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserRoles_RoleId",
+ table: "AspNetUserRoles",
+ column: "RoleId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AuthorizationCodes_ClientId_UserId",
+ table: "AuthorizationCodes",
+ columns: new[] { "ClientId", "UserId" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AuthorizationCodes_Code",
+ table: "AuthorizationCodes",
+ column: "Code",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Clients_ClientId",
+ table: "Clients",
+ column: "ClientId",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_RefreshTokens_ClientId",
+ table: "RefreshTokens",
+ column: "ClientId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_RefreshTokens_Token",
+ table: "RefreshTokens",
+ column: "Token",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_RefreshTokens_UserId",
+ table: "RefreshTokens",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "RoleNameIndex",
+ table: "Roles",
+ column: "NormalizedName",
+ unique: true,
+ filter: "[NormalizedName] IS NOT NULL");
+
+ migrationBuilder.CreateIndex(
+ name: "EmailIndex",
+ table: "Users",
+ column: "NormalizedEmail");
+
+ migrationBuilder.CreateIndex(
+ name: "UserNameIndex",
+ table: "Users",
+ column: "NormalizedUserName",
+ unique: true,
+ filter: "[NormalizedUserName] IS NOT NULL");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "ApiScopes");
+
+ migrationBuilder.DropTable(
+ name: "AspNetRoleClaims");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserClaims");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserLogins");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserRoles");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserTokens");
+
+ migrationBuilder.DropTable(
+ name: "AuthorizationCodes");
+
+ migrationBuilder.DropTable(
+ name: "RefreshTokens");
+
+ migrationBuilder.DropTable(
+ name: "ApiResources");
+
+ migrationBuilder.DropTable(
+ name: "Roles");
+
+ migrationBuilder.DropTable(
+ name: "Clients");
+
+ migrationBuilder.DropTable(
+ name: "Users");
+ }
+ }
+}
diff --git a/Authorization.API/Migrations/ApplicationDbContextModelSnapshot.cs b/Authorization.API/Migrations/ApplicationDbContextModelSnapshot.cs
new file mode 100644
index 0000000..da4baa7
--- /dev/null
+++ b/Authorization.API/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -0,0 +1,483 @@
+//
+using System;
+using Authorization.API.Context;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Authorization.API.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ partial class ApplicationDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Authorization.API.Models.ApiResource", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("ApiResources");
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.ApiScope", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ApiResourceId")
+ .HasColumnType("int");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ApiResourceId");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("ApiScopes");
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.ApplicationRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex")
+ .HasFilter("[NormalizedName] IS NOT NULL");
+
+ b.ToTable("Roles", (string)null);
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("int");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("FullName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("bit");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex")
+ .HasFilter("[NormalizedUserName] IS NOT NULL");
+
+ b.ToTable("Users", (string)null);
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.AuthorizationCode", b =>
+ {
+ b.Property("Code")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ClientId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("CodeChallenge")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CodeChallengeMethod")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime2");
+
+ b.Property("IsUsed")
+ .HasColumnType("bit");
+
+ b.Property("RedirectUri")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Scopes")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Subject")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Code");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("ClientId", "UserId");
+
+ b.ToTable("AuthorizationCodes");
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.Client", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AllowRefreshToken")
+ .HasColumnType("bit");
+
+ b.PrimitiveCollection("AllowedScopes")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClientId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ClientSecret")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PostLogoutRedirectUri")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RedirectUri")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RequirePkce")
+ .HasColumnType("bit");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId")
+ .IsUnique();
+
+ b.ToTable("Clients");
+ });
+
+ modelBuilder.Entity("Authorization.API.Models.RefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClientId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Created")
+ .HasColumnType("datetime2");
+
+ b.Property("Expiry")
+ .HasColumnType("datetime2");
+
+ b.Property("IsRevoked")
+ .HasColumnType("bit");
+
+ b.Property("Token")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId");
+
+ b.HasIndex("Token")
+ .IsUnique();
+
+ b.HasIndex("UserId");
+
+ b.ToTable("RefreshTokens");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderKey")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("RoleId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property