diff --git a/IdentityManager2.sln b/IdentityManager2.sln index e7e6430..20d2fee 100644 --- a/IdentityManager2.sln +++ b/IdentityManager2.sln @@ -1,61 +1,68 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29519.181 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{21035206-B373-4994-901B-2C9E882B5852}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdentityManager2", "src\IdentityManager2\IdentityManager2.csproj", "{E4718661-6384-4378-82C3-56D66F6E060F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts.LosthostAuthentication", "src\Hosts\Hosts.LosthostAuthentication\Hosts.LosthostAuthentication.csproj", "{D024CAF4-4371-45C5-9728-D06B462CD4B3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts.CookieAuthentication", "src\Hosts\Hosts.CookieAuthentication\Hosts.CookieAuthentication.csproj", "{20FEF10D-D003-48A7-A931-4DEB80796E08}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts.Shared", "src\Hosts\Hosts.Shared\Hosts.Shared.csproj", "{BE7A2243-B7D0-4D32-92F6-0DD4C8DA8208}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosts", "Hosts", "{59FA21EB-3472-4E3D-BDF0-AD32DCFA6035}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hosts.IdentityServerAuthentication", "src\Hosts\Hosts.IdentityServerAuthentication\Hosts.IdentityServerAuthentication.csproj", "{BAACD397-A69F-4F9B-A178-39D734CA78CC}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {E4718661-6384-4378-82C3-56D66F6E060F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E4718661-6384-4378-82C3-56D66F6E060F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E4718661-6384-4378-82C3-56D66F6E060F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E4718661-6384-4378-82C3-56D66F6E060F}.Release|Any CPU.Build.0 = Release|Any CPU - {D024CAF4-4371-45C5-9728-D06B462CD4B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D024CAF4-4371-45C5-9728-D06B462CD4B3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D024CAF4-4371-45C5-9728-D06B462CD4B3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D024CAF4-4371-45C5-9728-D06B462CD4B3}.Release|Any CPU.Build.0 = Release|Any CPU - {20FEF10D-D003-48A7-A931-4DEB80796E08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {20FEF10D-D003-48A7-A931-4DEB80796E08}.Debug|Any CPU.Build.0 = Debug|Any CPU - {20FEF10D-D003-48A7-A931-4DEB80796E08}.Release|Any CPU.ActiveCfg = Release|Any CPU - {20FEF10D-D003-48A7-A931-4DEB80796E08}.Release|Any CPU.Build.0 = Release|Any CPU - {BE7A2243-B7D0-4D32-92F6-0DD4C8DA8208}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BE7A2243-B7D0-4D32-92F6-0DD4C8DA8208}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BE7A2243-B7D0-4D32-92F6-0DD4C8DA8208}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BE7A2243-B7D0-4D32-92F6-0DD4C8DA8208}.Release|Any CPU.Build.0 = Release|Any CPU - {BAACD397-A69F-4F9B-A178-39D734CA78CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BAACD397-A69F-4F9B-A178-39D734CA78CC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BAACD397-A69F-4F9B-A178-39D734CA78CC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BAACD397-A69F-4F9B-A178-39D734CA78CC}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {E4718661-6384-4378-82C3-56D66F6E060F} = {21035206-B373-4994-901B-2C9E882B5852} - {D024CAF4-4371-45C5-9728-D06B462CD4B3} = {59FA21EB-3472-4E3D-BDF0-AD32DCFA6035} - {20FEF10D-D003-48A7-A931-4DEB80796E08} = {59FA21EB-3472-4E3D-BDF0-AD32DCFA6035} - {BE7A2243-B7D0-4D32-92F6-0DD4C8DA8208} = {59FA21EB-3472-4E3D-BDF0-AD32DCFA6035} - {59FA21EB-3472-4E3D-BDF0-AD32DCFA6035} = {21035206-B373-4994-901B-2C9E882B5852} - {BAACD397-A69F-4F9B-A178-39D734CA78CC} = {59FA21EB-3472-4E3D-BDF0-AD32DCFA6035} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {22A3DD5E-832A-4FFC-B0B9-1A3D07313154} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11111.16 d18.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{21035206-B373-4994-901B-2C9E882B5852}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdentityManager2", "src\IdentityManager2\IdentityManager2.csproj", "{E4718661-6384-4378-82C3-56D66F6E060F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts.LosthostAuthentication", "src\Hosts\Hosts.LosthostAuthentication\Hosts.LosthostAuthentication.csproj", "{D024CAF4-4371-45C5-9728-D06B462CD4B3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts.CookieAuthentication", "src\Hosts\Hosts.CookieAuthentication\Hosts.CookieAuthentication.csproj", "{20FEF10D-D003-48A7-A931-4DEB80796E08}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts.Shared", "src\Hosts\Hosts.Shared\Hosts.Shared.csproj", "{BE7A2243-B7D0-4D32-92F6-0DD4C8DA8208}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosts", "Hosts", "{59FA21EB-3472-4E3D-BDF0-AD32DCFA6035}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hosts.IdentityServerAuthentication", "src\Hosts\Hosts.IdentityServerAuthentication\Hosts.IdentityServerAuthentication.csproj", "{BAACD397-A69F-4F9B-A178-39D734CA78CC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityManager2.Apis", "src\IdentityManager2.Apis\IdentityManager2.Apis.csproj", "{A0B9D16D-1969-E880-1A5E-7BC666CB4178}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E4718661-6384-4378-82C3-56D66F6E060F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4718661-6384-4378-82C3-56D66F6E060F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4718661-6384-4378-82C3-56D66F6E060F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4718661-6384-4378-82C3-56D66F6E060F}.Release|Any CPU.Build.0 = Release|Any CPU + {D024CAF4-4371-45C5-9728-D06B462CD4B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D024CAF4-4371-45C5-9728-D06B462CD4B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D024CAF4-4371-45C5-9728-D06B462CD4B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D024CAF4-4371-45C5-9728-D06B462CD4B3}.Release|Any CPU.Build.0 = Release|Any CPU + {20FEF10D-D003-48A7-A931-4DEB80796E08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20FEF10D-D003-48A7-A931-4DEB80796E08}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20FEF10D-D003-48A7-A931-4DEB80796E08}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20FEF10D-D003-48A7-A931-4DEB80796E08}.Release|Any CPU.Build.0 = Release|Any CPU + {BE7A2243-B7D0-4D32-92F6-0DD4C8DA8208}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE7A2243-B7D0-4D32-92F6-0DD4C8DA8208}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE7A2243-B7D0-4D32-92F6-0DD4C8DA8208}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE7A2243-B7D0-4D32-92F6-0DD4C8DA8208}.Release|Any CPU.Build.0 = Release|Any CPU + {BAACD397-A69F-4F9B-A178-39D734CA78CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAACD397-A69F-4F9B-A178-39D734CA78CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAACD397-A69F-4F9B-A178-39D734CA78CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAACD397-A69F-4F9B-A178-39D734CA78CC}.Release|Any CPU.Build.0 = Release|Any CPU + {A0B9D16D-1969-E880-1A5E-7BC666CB4178}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0B9D16D-1969-E880-1A5E-7BC666CB4178}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0B9D16D-1969-E880-1A5E-7BC666CB4178}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0B9D16D-1969-E880-1A5E-7BC666CB4178}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {E4718661-6384-4378-82C3-56D66F6E060F} = {21035206-B373-4994-901B-2C9E882B5852} + {D024CAF4-4371-45C5-9728-D06B462CD4B3} = {59FA21EB-3472-4E3D-BDF0-AD32DCFA6035} + {20FEF10D-D003-48A7-A931-4DEB80796E08} = {59FA21EB-3472-4E3D-BDF0-AD32DCFA6035} + {BE7A2243-B7D0-4D32-92F6-0DD4C8DA8208} = {59FA21EB-3472-4E3D-BDF0-AD32DCFA6035} + {59FA21EB-3472-4E3D-BDF0-AD32DCFA6035} = {21035206-B373-4994-901B-2C9E882B5852} + {BAACD397-A69F-4F9B-A178-39D734CA78CC} = {59FA21EB-3472-4E3D-BDF0-AD32DCFA6035} + {A0B9D16D-1969-E880-1A5E-7BC666CB4178} = {21035206-B373-4994-901B-2C9E882B5852} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {22A3DD5E-832A-4FFC-B0B9-1A3D07313154} + EndGlobalSection +EndGlobal diff --git a/src/Hosts/Hosts.CookieAuthentication/Controllers/LoginController.cs b/src/Hosts/Hosts.CookieAuthentication/Controllers/LoginController.cs index 5ee615f..a164492 100644 --- a/src/Hosts/Hosts.CookieAuthentication/Controllers/LoginController.cs +++ b/src/Hosts/Hosts.CookieAuthentication/Controllers/LoginController.cs @@ -10,6 +10,7 @@ namespace Hosts.CookieAuthentication { + [Route("login")] public class LoginController : Controller { private readonly ICollection users; @@ -19,13 +20,13 @@ public LoginController(ICollection users) this.users = users ?? throw new ArgumentNullException(nameof(users)); } - [HttpGet("login")] + [HttpGet(Name = "CookieAuthentication-login")] public IActionResult Login(string returnUrl) { return View(new LoginModel {ReturnUrl = returnUrl}); } - [HttpPost("login")] + [HttpPost("CookieAuthentication-login")] [ValidateAntiForgeryToken] public async Task Login(LoginModel model) { diff --git a/src/Hosts/Hosts.CookieAuthentication/Program.cs b/src/Hosts/Hosts.CookieAuthentication/Program.cs index 34db2af..1f929cb 100644 --- a/src/Hosts/Hosts.CookieAuthentication/Program.cs +++ b/src/Hosts/Hosts.CookieAuthentication/Program.cs @@ -1,16 +1,18 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; - -namespace Hosts.CookieAuthentication -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); - } -} +using Microsoft.AspNetCore.Builder; + +namespace Hosts.CookieAuthentication; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + builder.Services.ConfigureServices(); + + var app = builder.Build(); + app.Configure(); + + app.Run(); + } + +} diff --git a/src/Hosts/Hosts.CookieAuthentication/Properties/launchSettings.json b/src/Hosts/Hosts.CookieAuthentication/Properties/launchSettings.json index c8df2d8..c2cbd90 100644 --- a/src/Hosts/Hosts.CookieAuthentication/Properties/launchSettings.json +++ b/src/Hosts/Hosts.CookieAuthentication/Properties/launchSettings.json @@ -3,7 +3,8 @@ "Hosts.CookieAuthentication": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "http://localhost:5000", + "launchUrl": "", + "applicationUrl": "https://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Hosts/Hosts.CookieAuthentication/Startup.cs b/src/Hosts/Hosts.CookieAuthentication/Startup.cs index c50fcf7..c2bc874 100644 --- a/src/Hosts/Hosts.CookieAuthentication/Startup.cs +++ b/src/Hosts/Hosts.CookieAuthentication/Startup.cs @@ -1,50 +1,52 @@ -using System; -using Hosts.Shared.InMemory; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace Hosts.CookieAuthentication -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - // In-memory IdentityManagerService (demo only) - services.AddIdentityManager(options => - { - options.SecurityConfiguration.HostAuthenticationType = "cookie"; - options.SecurityConfiguration.HostChallengeType = "cookie"; - }) - .AddIdentityMangerService(); - - var rand = new Random(); - services.AddSingleton(x => Users.Get(rand.Next(5000, 20000))); - services.AddSingleton(x => Roles.Get(rand.Next(15))); - - services.AddAuthentication("cookie") - .AddCookie("cookie", options => - { - options.LoginPath = "/login"; - }); - } - - public void Configure(IApplicationBuilder app) - { - app.UseDeveloperExceptionPage(); - - app.UseRouting(); - - app.UseStaticFiles(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseIdentityManager(); - - app.UseEndpoints(x => - { - x.MapDefaultControllerRoute(); - }); - } - } -} +using System; +using Hosts.Shared.InMemory; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Hosts.CookieAuthentication; + +public static class Startup +{ + public static void ConfigureServices(this IServiceCollection services) + { + // In-memory IdentityManagerService (demo only) + services.AddIdentityManager(options => + { + options.SecurityConfiguration.HostAuthenticationType = "cookie"; + options.SecurityConfiguration.HostChallengeType = "cookie"; + }) + .AddIdentityMangerService() + .AddIdentityManagerUI(); + + var rand = new Random(); + services.AddSingleton(x => Users.Get(rand.Next(5000, 20000))); + services.AddSingleton(x => Roles.Get(rand.Next(15))); + + services + .AddAuthentication("cookie") + .AddCookie("cookie", options => + { + options.LoginPath = "/login"; + }); + } + + public static void Configure(this WebApplication app) + { + app.UseDeveloperExceptionPage(); + + app.UseRouting(); + + app.UseStaticFiles(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseIdentityManager(); + + // Map attribute-routed controllers first + app.MapControllers(); + + app.MapIdentityManagerUI(string.Empty); // set "launchUrl": "", in launchSettings.json + + } +} diff --git a/src/Hosts/Hosts.IdentityServerAuthentication/Controllers/LoginController.cs b/src/Hosts/Hosts.IdentityServerAuthentication/Controllers/LoginController.cs index 43100b1..6f2b5a3 100644 --- a/src/Hosts/Hosts.IdentityServerAuthentication/Controllers/LoginController.cs +++ b/src/Hosts/Hosts.IdentityServerAuthentication/Controllers/LoginController.cs @@ -13,6 +13,7 @@ namespace Hosts.IdentityServerAuthentication { + [Route("login")] public class LoginController : Controller { private readonly ICollection users; diff --git a/src/Hosts/Hosts.IdentityServerAuthentication/Program.cs b/src/Hosts/Hosts.IdentityServerAuthentication/Program.cs index ad523a7..f7d80a1 100644 --- a/src/Hosts/Hosts.IdentityServerAuthentication/Program.cs +++ b/src/Hosts/Hosts.IdentityServerAuthentication/Program.cs @@ -1,16 +1,18 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Builder; -namespace Hosts.IdentityServerAuthentication +namespace Hosts.IdentityServerAuthentication; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } + var builder = WebApplication.CreateBuilder(args); + + Startup.ConfigureServices(builder.Services); + + var app = builder.Build(); + app.Configure(); - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); + app.Run(); } } diff --git a/src/Hosts/Hosts.IdentityServerAuthentication/Startup.cs b/src/Hosts/Hosts.IdentityServerAuthentication/Startup.cs index deac682..f9d1f32 100644 --- a/src/Hosts/Hosts.IdentityServerAuthentication/Startup.cs +++ b/src/Hosts/Hosts.IdentityServerAuthentication/Startup.cs @@ -1,110 +1,115 @@ -using System; -using System.Collections.Generic; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using Duende.IdentityServer.Models; -using Duende.IdentityServer.Test; -using Hosts.Shared.InMemory; -using IdentityManager2.Configuration; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace Hosts.IdentityServerAuthentication -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - // In-memory IdentityManagerService (demo only) - services.AddIdentityManager(opt => - opt.SecurityConfiguration = - new SecurityConfiguration - { - HostAuthenticationType = "cookie", - HostChallengeType = "oidc", - AdditionalSignOutType = "oidc" - }) - .AddIdentityMangerService(); - - var rand = new Random(); - var identityManagerUsers = Users.Get(rand.Next(5000, 20000)); - services.AddSingleton(x => identityManagerUsers); - services.AddSingleton(x => Roles.Get(rand.Next(15))); - - var client = new Client - { - ClientId = "identitymanager2", - ClientName = "IdentityManager2", - AllowedGrantTypes = GrantTypes.Implicit, - RedirectUris = {"https://localhost:5000/idm/signin-oidc"}, - AllowedScopes = {"openid", "profile", "roles"}, - RequireConsent = false - }; - - var roles = new IdentityResource("roles", new List {"role"}); - - var identityServerUsers = identityManagerUsers.Select(x => new TestUser - { - SubjectId = x.Subject, - Username = x.Username, - Password = x.Password, - Claims = x.Claims - }).ToList(); - - services.AddIdentityServer(options => - { - options.UserInteraction.LoginUrl = "/login"; - options.UserInteraction.LogoutUrl = "/logout"; - }) - .AddTestUsers(identityServerUsers) - .AddInMemoryIdentityResources(new List {new IdentityResources.OpenId(), new IdentityResources.Profile(), roles}) - .AddInMemoryApiResources(new List()) - .AddInMemoryClients(new List {client}) - .AddDeveloperSigningCredential(false); - - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - - services.AddAuthentication() - .AddCookie("cookie") - .AddOpenIdConnect("oidc", opt => - { - opt.Authority = "https://localhost:5000/auth"; - opt.ClientId = "identitymanager2"; - - // default: openid & profile - opt.Scope.Add("roles"); - - opt.RequireHttpsMetadata = false; // dev only - opt.SignInScheme = "cookie"; - opt.CallbackPath = "/signin-oidc"; - }); - } - - public void Configure(IApplicationBuilder app) - { - app.UseDeveloperExceptionPage(); - - app.Map("/auth", auth => - { - auth.UseRouting(); - - auth.UseIdentityServer(); - - auth.UseEndpoints(x => x.MapDefaultControllerRoute()); - }); - - app.Map("/idm", idm => - { - idm.UseRouting(); - - idm.UseAuthentication(); - idm.UseAuthorization(); - - idm.UseIdentityManager(); - - idm.UseEndpoints(x => x.MapDefaultControllerRoute()); - }); - - } - } +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Test; +using Hosts.Shared.InMemory; +using IdentityManager2.Configuration; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Hosts.IdentityServerAuthentication +{ + public static class Startup + { + public static void ConfigureServices(this IServiceCollection services) + { + // In-memory IdentityManagerService (demo only) + services.AddIdentityManager(opt => + opt.SecurityConfiguration = + new SecurityConfiguration + { + HostAuthenticationType = "cookie", + HostChallengeType = "oidc", + AdditionalSignOutType = "oidc" + }) + .AddIdentityMangerService() + .AddIdentityManagerUI(); + + var rand = new Random(); + var identityManagerUsers = Users.Get(rand.Next(5000, 20000)); + services.AddSingleton(x => identityManagerUsers); + services.AddSingleton(x => Roles.Get(rand.Next(15))); + + var client = new Client + { + ClientId = "identitymanager2", + ClientName = "IdentityManager2", + AllowedGrantTypes = GrantTypes.Implicit, + RedirectUris = { "https://localhost:5000/idm/signin-oidc" }, + AllowedScopes = { "openid", "profile", "roles" }, + RequireConsent = false + }; + + var roles = new IdentityResource("roles", new List { "role" }); + + var identityServerUsers = identityManagerUsers.Select(x => new TestUser + { + SubjectId = x.Subject, + Username = x.Username, + Password = x.Password, + Claims = x.Claims + }).ToList(); + + services.AddIdentityServer(options => + { + options.UserInteraction.LoginUrl = "/login"; + options.UserInteraction.LogoutUrl = "/logout"; + }) + .AddTestUsers(identityServerUsers) + .AddInMemoryIdentityResources(new List { new IdentityResources.OpenId(), new IdentityResources.Profile(), roles }) + .AddInMemoryApiResources(new List()) + .AddInMemoryClients(new List { client }) + .AddDeveloperSigningCredential(false); + + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + + services.AddAuthentication() + .AddCookie("cookie") + .AddOpenIdConnect("oidc", opt => + { + opt.Authority = "https://localhost:5000/auth"; + opt.ClientId = "identitymanager2"; + + // default: openid & profile + opt.Scope.Add("roles"); + + opt.RequireHttpsMetadata = false; // dev only + opt.SignInScheme = "cookie"; + opt.CallbackPath = "/signin-oidc"; + }); + } + + public static void Configure(this WebApplication app) + { + app.UseDeveloperExceptionPage(); + + app.Map("/auth", auth => + { + auth.UseRouting(); + + auth.UseIdentityServer(); + + auth.UseEndpoints(x => x.MapDefaultControllerRoute()); + }); + + app.MapIdentityManagerUI("idm"); // set "launchUrl": "idm", in launchSettings.json + + app.Map("/idm", idm => + { + idm.UseRouting(); + + idm.UseAuthentication(); + idm.UseAuthorization(); + + idm.UseIdentityManager(); + + idm.UseEndpoints(x => + { + x.MapDefaultControllerRoute(); + }); + }); + } + } } \ No newline at end of file diff --git a/src/Hosts/Hosts.LosthostAuthentication/Program.cs b/src/Hosts/Hosts.LosthostAuthentication/Program.cs index 5b69d68..a2b45f7 100644 --- a/src/Hosts/Hosts.LosthostAuthentication/Program.cs +++ b/src/Hosts/Hosts.LosthostAuthentication/Program.cs @@ -1,16 +1,40 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; - -namespace Hosts.LosthostAuthentication -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); - } -} +using Hosts.Shared.InMemory; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; + +namespace Hosts.LosthostAuthentication; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // In-memory IdentityManagerService (demo only) + builder.Services + .AddIdentityManager() + .AddIdentityMangerService() + .AddIdentityManagerUI(); + + var rand = new Random(); + builder.Services.AddSingleton(x => Users.Get(rand.Next(5000, 20000))); + builder.Services.AddSingleton(x => Roles.Get(rand.Next(15))); + + var app = builder.Build(); + app.UseStaticFiles(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseIdentityManager(); + app.MapIdentityManagerUI("idmgr2"); // set "launchUrl": "idmgr2", in launchSettings.json + + app.Run(); + } +} diff --git a/src/Hosts/Hosts.LosthostAuthentication/Properties/launchSettings.json b/src/Hosts/Hosts.LosthostAuthentication/Properties/launchSettings.json index bd93e85..f823db0 100644 --- a/src/Hosts/Hosts.LosthostAuthentication/Properties/launchSettings.json +++ b/src/Hosts/Hosts.LosthostAuthentication/Properties/launchSettings.json @@ -4,6 +4,7 @@ "commandName": "Project", "launchBrowser": true, "applicationUrl": "http://localhost:5000", + "launchUrl": "idmgr2", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Hosts/Hosts.LosthostAuthentication/Startup.cs b/src/Hosts/Hosts.LosthostAuthentication/Startup.cs deleted file mode 100644 index 4f8bc3b..0000000 --- a/src/Hosts/Hosts.LosthostAuthentication/Startup.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using System; -using Hosts.Shared.InMemory; - -namespace Hosts.LosthostAuthentication -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - // In-memory IdentityManagerService (demo only) - services.AddIdentityManager() - .AddIdentityMangerService(); - - var rand = new Random(); - services.AddSingleton(x => Users.Get(rand.Next(5000, 20000))); - services.AddSingleton(x => Roles.Get(rand.Next(15))); - } - - public void Configure(IApplicationBuilder app) - { - app.UseDeveloperExceptionPage(); - - app.UseRouting(); - - app.UseStaticFiles(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseIdentityManager(); - - app.UseEndpoints(x => - { - x.MapDefaultControllerRoute(); - }); - } - } -} diff --git a/src/Hosts/Hosts.Shared/InMemory/InMemoryIdentityManagerService.cs b/src/Hosts/Hosts.Shared/InMemory/InMemoryIdentityManagerService.cs index 7eb7a82..4413f7c 100644 --- a/src/Hosts/Hosts.Shared/InMemory/InMemoryIdentityManagerService.cs +++ b/src/Hosts/Hosts.Shared/InMemory/InMemoryIdentityManagerService.cs @@ -6,7 +6,7 @@ using IdentityManager2.Core; using IdentityManager2.Core.Metadata; using IdentityManager2.Extensions; -using IdentityManager2.Resources; +using IdentityManager2.Apis.Resources; namespace Hosts.Shared.InMemory { diff --git a/src/IdentityManager2.Apis/Api/Controllers/MetaController.cs b/src/IdentityManager2.Apis/Api/Controllers/MetaController.cs new file mode 100644 index 0000000..ab21f61 --- /dev/null +++ b/src/IdentityManager2.Apis/Api/Controllers/MetaController.cs @@ -0,0 +1,80 @@ +using IdentityManager2.Api.Models; +using IdentityManager2.Core.Metadata; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace IdentityManager2.Api.Controllers +{ + // TOOD: [Route("api/[area:exists]/[controller]")] + [Route(IdentityManagerConstants.MetadataRoutePrefix)] + public class MetaController : BaseApiController + { + #region Constructors + + public MetaController(IIdentityManagerService service, ILogger logger) : base(service, logger) { } + + #endregion + + #region Endpoints + + [Route("")] + [HttpGet] + [EndpointName("metadata-list-get")] + [EndpointSummary("This is a summary.")] + [EndpointDescription("This is a description.")] + [Tags(["metadata"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status200OK, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + public async Task Get() + { + logger.LogInformation("Get metadata called by user: {Username}", User.Identity?.Name); + + logger.LogDebug("Retrieving metadata from service"); + var meta = await GetMetadataAsync(); + + logger.LogDebug("Building metadata response. User supports: Create={SupportsUserCreate}, Delete={SupportsUserDelete}, Claims={SupportsClaims}", + meta.UserMetadata.SupportsCreate, + meta.UserMetadata.SupportsDelete, + meta.UserMetadata.SupportsClaims); + + logger.LogDebug("Role supports: Listing={SupportsRoleListing}, Create={SupportsRoleCreate}, Delete={SupportsRoleDelete}", + meta.RoleMetadata.SupportsListing, + meta.RoleMetadata.SupportsCreate, + meta.RoleMetadata.SupportsDelete); + + var data = new Dictionary { { "currentUser", new AnonymousUserName { username = User.Identity.Name } } }; + + var links = new Dictionary { ["users"] = Url.Link("GetUsers", null) }; + + if (meta.RoleMetadata.SupportsListing) + { + logger.LogDebug("Adding roles link to metadata"); + links["roles"] = Url.Link("GetRoles", null); + } + if (meta.UserMetadata.SupportsCreate) + { + logger.LogDebug("Adding createUser link to metadata"); + links["createUser"] = new CreateUserLink(Url, meta.UserMetadata); + } + if (meta.RoleMetadata.SupportsCreate) + { + logger.LogDebug("Adding createRole link to metadata"); + links["createRole"] = new CreateRoleLink(Url, meta.RoleMetadata); + } + + logger.LogInformation("Successfully retrieved metadata with {LinkCount} links", links.Count); + return Ok(new MetaResult + { + Data = data, + Links = links + }); + } + + #endregion + } +} diff --git a/src/IdentityManager2.Apis/Api/Controllers/RolesController.cs b/src/IdentityManager2.Apis/Api/Controllers/RolesController.cs new file mode 100644 index 0000000..f2e068a --- /dev/null +++ b/src/IdentityManager2.Apis/Api/Controllers/RolesController.cs @@ -0,0 +1,322 @@ +using IdentityManager2.Api.Models; +using IdentityManager2.Core; +using IdentityManager2.Core.Metadata; +using IdentityManager2.Extensions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using static System.String; + +namespace IdentityManager2.Api.Controllers +{ + // TOOD: [Route("api/[area:exists]/[controller]")] + [Route(IdentityManagerConstants.RoleRoutePrefix)] + public class RolesController : BaseApiController + { + #region Constructors + + public RolesController(IIdentityManagerService service, ILogger logger) : base(service, logger) { } + + #endregion + + #region Endpoints + + // GET api/roles + [HttpGet] + [Route("", Name = IdentityManagerConstants.RouteNames.GetRoles)] + [EndpointName("roles-get-roles")] + [EndpointSummary("TODO.")] + [EndpointDescription("TODO.")] + [Tags(["roles"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status200OK, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status405MethodNotAllowed, "application/problem+json")] + public async Task GetRolesAsync(string filter = null, int start = 0, int count = 100) + { + logger.LogInformation("GetRolesAsync called with filter: {Filter}, start: {Start}, count: {Count}", filter, start, count); + + var meta = await GetMetadataAsync(); + if (!meta.RoleMetadata.SupportsListing) + { + logger.LogWarning("Role listing is not supported"); + return MethodNotAllowed(); + } + + logger.LogDebug("Querying roles from service"); + var result = await service.QueryRolesAsync(filter, start, count); + if (result.IsSuccess) + { + try + { + logger.LogInformation("Successfully retrieved {Count} roles", result.Result?.Items?.Count ?? 0); + return Ok(new RoleQueryResultResource(result.Result, Url, meta.RoleMetadata)); + } + catch (Exception exp) + { + logger.LogError(exp, "Exception occurred while creating RoleQueryResultResource"); + throw new ArgumentNullException(exp.ToString()); + } + } + + logger.LogWarning("Failed to query roles. Errors: {Errors}", string.Join(", ", result.Errors ?? new List())); + return BadRequest(result.ToError()); + } + + // POST + [HttpPost] + [Route("", Name = IdentityManagerConstants.RouteNames.CreateRole)] + [EndpointName("roles-create-role")] + [EndpointSummary("TODO.")] + [EndpointDescription("TODO.")] + [Tags(["roles"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status201Created, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status405MethodNotAllowed, "application/problem+json")] + public async Task CreateRoleAsync([FromBody] PropertyValue[] properties) + { + logger.LogInformation("CreateRoleAsync called with {PropertyCount} properties", properties?.Length ?? 0); + + var meta = await GetMetadataAsync(); + if (!meta.RoleMetadata.SupportsCreate) + { + logger.LogWarning("Role creation is not supported"); + return MethodNotAllowed(); + } + + logger.LogDebug("Validating create properties"); + var errors = ValidateCreateProperties(meta.RoleMetadata, properties); + + foreach (var error in errors) + { + logger.LogWarning("Validation error: {Error}", error); + ModelState.AddModelError("", error); + } + + if (ModelState.IsValid) + { + logger.LogDebug("Creating role via service"); + var result = await service.CreateRoleAsync(properties); + if (result.IsSuccess) + { + logger.LogInformation("Successfully created role with subject: {Subject}", result.Result.Subject); + var url = Url.Link(IdentityManagerConstants.RouteNames.GetRole, new AnonymousSubject { subject = result.Result.Subject }); + + var resource = new AnonymousCreatedRole + { + Data = new AnonymousSubject { subject = result.Result.Subject }, + Links = new AnonymousDetail { detail = url } + }; + return Created(url, resource); + } + + logger.LogWarning("Failed to create role. Errors: {Errors}", string.Join(", ", result.Errors ?? new List())); + ModelState.AddModelError("", errors.ToString()); + } + + logger.LogWarning("ModelState is invalid when creating role"); + return BadRequest(ModelState.ToError()); + } + + [HttpGet("{subject}", Name = IdentityManagerConstants.RouteNames.GetRole)] + [EndpointName("roles-get-role")] + [EndpointSummary("TODO.")] + [EndpointDescription("TODO.")] + [Tags(["roles"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status200OK, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status405MethodNotAllowed, "application/problem+json")] + public async Task GetRoleAsync(string subject) + { + logger.LogInformation("GetRoleAsync called for subject: {Subject}", subject); + + if (IsNullOrWhiteSpace(subject)) + { + logger.LogWarning("Subject is null or whitespace"); + ModelState["subject.String"]?.Errors.Clear(); + ModelState.AddModelError("", Messages.SubjectRequired); + } + + if (!ModelState.IsValid) + { + logger.LogWarning("ModelState is invalid"); + return BadRequest(ModelState); + } + + var meta = await GetMetadataAsync(); + if (!meta.RoleMetadata.SupportsListing) + { + logger.LogWarning("Role listing is not supported"); + return MethodNotAllowed(); + } + + logger.LogDebug("Getting role from service"); + var result = await service.GetRoleAsync(subject); + + if (result.IsSuccess) + { + if (result.Result == null) + { + logger.LogWarning("Role not found for subject: {Subject}", subject); + return NotFound(); + } + + logger.LogInformation("Successfully retrieved role: {RoleName}", result.Result.Name); + var response = Ok(new RoleDetailResource(result.Result, Url, meta.RoleMetadata)); + return response; + } + + logger.LogWarning("Failed to get role. Errors: {Errors}", string.Join(", ", result.Errors ?? new List())); + return BadRequest(result.ToError()); + } + + [HttpDelete] + [Route("{subject}", Name = IdentityManagerConstants.RouteNames.DeleteRole)] + [EndpointName("roles-delete-role")] + [EndpointSummary("TODO.")] + [EndpointDescription("TODO.")] + [Tags(["roles"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status204NoContent, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + public async Task DeleteRoleAsync(string subject) + { + logger.LogInformation("DeleteRoleAsync called for subject: {Subject}", subject); + + var meta = await GetMetadataAsync(); + if (!meta.RoleMetadata.SupportsDelete) + { + logger.LogWarning("Role deletion is not supported"); + return MethodNotAllowed(); + } + + if (!ModelState.IsValid) + { + logger.LogWarning("ModelState is invalid"); + return BadRequest(ModelState.ToError()); + } + + logger.LogDebug("Deleting role via service"); + var result = await service.DeleteRoleAsync(subject); + if (result.IsSuccess) + { + logger.LogInformation("Successfully deleted role with subject: {Subject}", subject); + return NoContent(); + } + + logger.LogWarning("Failed to delete role. Errors: {Errors}", string.Join(", ", result.Errors ?? new List())); + return BadRequest(result.ToError()); + } + + [HttpPut] + [Route("{subject}/properties/{type}", Name = IdentityManagerConstants.RouteNames.UpdateRoleProperty)] + [EndpointName("roles-set-property")] + [EndpointSummary("TODO.")] + [EndpointDescription("TODO.")] + [Tags(["roles"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status204NoContent, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + public async Task SetPropertyAsync(string subject, string type) + { + logger.LogInformation("SetPropertyAsync called for subject: {Subject}, type: {Type}", subject, type); + + if (IsNullOrWhiteSpace(subject)) + { + logger.LogWarning("Subject is null or whitespace"); + ModelState["subject.String"]?.Errors.Clear(); + ModelState.AddModelError("", Messages.SubjectRequired); + } + + type = type.FromBase64UrlEncoded(); + var value = await Request.Body.ReadAsStringAsync(); + + logger.LogDebug("Decoded property type: {Type}, value length: {ValueLength}", type, value?.Length ?? 0); + + var meta = await GetMetadataAsync(); + + ValidateUpdateProperty(meta.RoleMetadata, type, value); + + if (ModelState.IsValid) + { + logger.LogDebug("Setting role property via service"); + var result = await service.SetRolePropertyAsync(subject, type, value); + + if (result.IsSuccess) + { + logger.LogInformation("Successfully set property {Type} for role {Subject}", type, subject); + return NoContent(); + } + + logger.LogWarning("Failed to set role property. Errors: {Errors}", string.Join(", ", result.Errors ?? new List())); + ModelState.AddErrors(result); + } + + logger.LogWarning("ModelState is invalid when setting role property"); + return BadRequest(ModelState.ToError()); + } + + #endregion + + #region Helpers + + [NonAction] + private IEnumerable ValidateCreateProperties(RoleMetadata roleMetadata, IEnumerable properties) + { + if (roleMetadata == null) throw new ArgumentNullException(nameof(roleMetadata)); + properties = properties ?? Enumerable.Empty(); + + var meta = roleMetadata.GetCreateProperties(); + return meta.Validate(properties); + } + + [NonAction] + private void ValidateUpdateProperty(RoleMetadata roleMetadata, string type, string value) + { + if (roleMetadata == null) throw new ArgumentNullException(nameof(roleMetadata)); + + if (IsNullOrWhiteSpace(type)) + { + logger.LogWarning("Property type is required but was not provided"); + ModelState.AddModelError("", Messages.PropertyTypeRequired); + return; + } + + var prop = roleMetadata.UpdateProperties.SingleOrDefault(x => x.Type == type); + if (prop == null) + { + logger.LogWarning("Invalid property type: {Type}", type); + ModelState.AddModelError("", Format(Messages.PropertyInvalid, type)); + } + else + { + var error = prop.Validate(value); + if (error != null) + { + logger.LogWarning("Property validation failed for type {Type}: {Error}", type, error); + ModelState.AddModelError("", error); + } + } + } + + [NonAction] + private IActionResult MethodNotAllowed() + { + return StatusCode(405); + } + + #endregion + } +} diff --git a/src/IdentityManager2.Apis/Api/Controllers/UsersController.cs b/src/IdentityManager2.Apis/Api/Controllers/UsersController.cs new file mode 100644 index 0000000..67a4f2c --- /dev/null +++ b/src/IdentityManager2.Apis/Api/Controllers/UsersController.cs @@ -0,0 +1,509 @@ +using IdentityManager2.Api.Models; +using IdentityManager2.Core; +using IdentityManager2.Core.Metadata; +using IdentityManager2.Extensions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using static System.String; + +namespace IdentityManager2.Api.Controllers +{ + // TOOD: [Route("api/[area:exists]/[controller]")] + [Route(IdentityManagerConstants.UserRoutePrefix)] + public class UsersController : BaseApiController + { + #region Constructors + + public UsersController(IIdentityManagerService service, ILogger logger) : base(service, logger) { } + + #endregion + + #region Endpoints + + [HttpGet] + [Route("", Name = IdentityManagerConstants.RouteNames.GetUsers)] + [EndpointName("users-get-users")] + [EndpointSummary("TODO.")] + [EndpointDescription("TODO.")] + [Tags(["users"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status200OK, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status405MethodNotAllowed, "application/problem+json")] + public async Task GetUsersAsync(string filter = null, int start = 0, int count = 100) + { + logger.LogInformation("GetUsersAsync called with filter: {Filter}, start: {Start}, count: {Count}", filter, start, count); + + logger.LogDebug("Querying users from service"); + var result = await service.QueryUsersAsync(filter, start, count); + if (result.IsSuccess) + { + logger.LogInformation("Successfully retrieved {Count} users", result.Result?.Items?.Count ?? 0); + var meta = await GetMetadataAsync(); + + var resource = new UserQueryResultResource(result.Result, Url, meta.UserMetadata); + return Ok(resource); + } + + logger.LogWarning("Failed to query users. Errors: {Errors}", string.Join(", ", result.Errors ?? new List())); + return BadRequest(result.ToError()); + } + + [HttpPost("", Name = IdentityManagerConstants.RouteNames.CreateUser)] + [EndpointName("users-create-user")] + [EndpointSummary("TODO.")] + [EndpointDescription("TODO.")] + [Tags(["users"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status201Created, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status405MethodNotAllowed, "application/problem+json")] + public async Task CreateUserAsync([FromBody] PropertyValue[] properties) + { + logger.LogInformation("CreateUserAsync called with {PropertyCount} properties", properties?.Length ?? 0); + + var meta = await GetMetadataAsync(); + if (!meta.UserMetadata.SupportsCreate) + { + logger.LogWarning("User creation is not supported"); + return MethodNotAllowed(); + } + + logger.LogDebug("Validating create properties"); + var errors = ValidateCreateProperties(meta.UserMetadata, properties); + + foreach (var error in errors) + { + logger.LogWarning("Validation error: {Error}", error); + ModelState.AddModelError("", error); + } + + if (ModelState.IsValid) + { + logger.LogDebug("Creating user via service"); + var result = await service.CreateUserAsync(properties); + if (result.IsSuccess) + { + logger.LogInformation("Successfully created user with subject: {Subject}", result.Result.Subject); + var url = Url.Link(IdentityManagerConstants.RouteNames.GetUser, new AnonymousSubject { subject = result.Result.Subject }); + var resource = new AnonymousCreatedUser + { + Data = new AnonymousSubject { subject = result.Result.Subject }, + Links = new AnonymousDetail { detail = url } + }; + + return Created(url, resource); + } + + logger.LogWarning("Failed to create user. Errors: {Errors}", string.Join(", ", result.Errors ?? new List())); + ModelState.AddModelError("errors", result.Errors.Aggregate((workingSentence, next) => workingSentence + " " + next)); + if (result.Errors.Count > 0) + return BadRequest(ModelState); + } + + logger.LogWarning("ModelState is invalid when creating user"); + return BadRequest(400); + } + + [HttpGet("{subject}", Name = IdentityManagerConstants.RouteNames.GetUser)] + [EndpointName("users-get-user")] + [EndpointSummary("TODO.")] + [EndpointDescription("TODO.")] + [Tags(["users"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status200OK, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status405MethodNotAllowed, "application/problem+json")] + public async Task GetUserAsync(string subject) + { + logger.LogInformation("GetUserAsync called for subject: {Subject}", subject); + + if (IsNullOrWhiteSpace(subject)) + { + logger.LogWarning("Subject is null or whitespace"); + ModelState["subject.String"]?.Errors.Clear(); + ModelState.AddModelError("", Messages.SubjectRequired); + } + + if (!ModelState.IsValid) + { + logger.LogWarning("ModelState is invalid"); + return BadRequest(ModelState); + } + + logger.LogDebug("Getting user from service"); + var result = await service.GetUserAsync(subject); + if (result.IsSuccess) + { + if (result.Result == null) + { + logger.LogWarning("User not found for subject: {Subject}", subject); + return NotFound(); + } + + logger.LogInformation("Successfully retrieved user: {Username}", result.Result.Username); + var meta = await GetMetadataAsync(); + RoleSummary[] roles = null; + if (!IsNullOrWhiteSpace(meta.RoleMetadata.RoleClaimType)) + { + logger.LogDebug("Querying roles for user detail"); + var roleResult = await service.QueryRolesAsync(null, -1, -1); + if (!roleResult.IsSuccess) + { + logger.LogWarning("Failed to query roles. Errors: {Errors}", string.Join(", ", roleResult.Errors ?? new List())); + return BadRequest(roleResult.Errors); + } + + roles = roleResult.Result.Items.ToArray(); + logger.LogDebug("Retrieved {RoleCount} roles for user detail", roles.Length); + } + + return Ok(new UserDetailResource(result.Result, Url, meta, roles)); + } + + logger.LogWarning("Failed to get user. Errors: {Errors}", string.Join(", ", result.Errors ?? new List())); + return BadRequest(result.ToError()); + } + + [HttpDelete] + [Route("{subject}", Name = IdentityManagerConstants.RouteNames.DeleteUser)] + [EndpointName("users-delete-user")] + [EndpointSummary("TODO.")] + [EndpointDescription("TODO.")] + [Tags(["users"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status204NoContent, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + public async Task DeleteUserAsync(string subject) + { + logger.LogInformation("DeleteUserAsync called for subject: {Subject}", subject); + + var meta = await GetMetadataAsync(); + if (!meta.UserMetadata.SupportsDelete) + { + logger.LogWarning("User deletion is not supported"); + return MethodNotAllowed(); + } + + if (IsNullOrWhiteSpace(subject)) + { + logger.LogWarning("Subject is null or whitespace"); + ModelState["subject.String"]?.Errors.Clear(); + ModelState.AddModelError("", Messages.SubjectRequired); + } + + if (!ModelState.IsValid) + { + logger.LogWarning("ModelState is invalid"); + return BadRequest(ModelState.ToError()); + } + + logger.LogDebug("Deleting user via service"); + var result = await service.DeleteUserAsync(subject); + if (result.IsSuccess) + { + logger.LogInformation("Successfully deleted user with subject: {Subject}", subject); + return NoContent(); + } + + logger.LogWarning("Failed to delete user. Errors: {Errors}", string.Join(", ", result.Errors ?? new List())); + return BadRequest(result.ToError()); + } + + [HttpPut] + [Route("{subject}/properties/{type}", Name = IdentityManagerConstants.RouteNames.UpdateUserProperty)] + [EndpointName("users-set-property")] + [EndpointSummary("TODO.")] + [EndpointDescription("TODO.")] + [Tags(["users"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status204NoContent, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + public async Task SetPropertyAsync(string subject, string type) + { + logger.LogInformation("SetPropertyAsync called for subject: {Subject}, type: {Type}", subject, type); + + if (IsNullOrWhiteSpace(subject)) + { + logger.LogWarning("Subject is null or whitespace"); + ModelState["subject.String"]?.Errors.Clear(); + ModelState.AddModelError("", Messages.SubjectRequired); + } + + type = type.FromBase64UrlEncoded(); + + var value = await Request.Body.ReadAsStringAsync(); + + logger.LogDebug("Decoded property type: {Type}, value length: {ValueLength}", type, value?.Length ?? 0); + + var meta = await GetMetadataAsync(); + ValidateUpdateProperty(meta.UserMetadata, type, value); + + if (ModelState.IsValid) + { + logger.LogDebug("Setting user property via service"); + var result = await service.SetUserPropertyAsync(subject, type, value); + if (result.IsSuccess) + { + logger.LogInformation("Successfully set property {Type} for user {Subject}", type, subject); + return NoContent(); + } + + logger.LogWarning("Failed to set user property. Errors: {Errors}", string.Join(", ", result.Errors ?? new List())); + ModelState.AddErrors(result); + } + + logger.LogWarning("ModelState is invalid when setting user property"); + return BadRequest(ModelState.ToError()); + } + + #region Claims and Roles + + [HttpPost] + [Route("{subject}/claims", Name = IdentityManagerConstants.RouteNames.AddClaim)] + [EndpointName("users-add-claim")] + [EndpointSummary("TODO.")] + [EndpointDescription("TODO.")] + [Tags(["users"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status204NoContent, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + public async Task AddClaimAsync(string subject, [FromBody] ClaimValue model) + { + logger.LogInformation("AddClaimAsync called for subject: {Subject}, claim type: {ClaimType}", subject, model?.Type); + + var meta = await GetMetadataAsync(); + if (!meta.UserMetadata.SupportsClaims) + { + logger.LogWarning("User claims are not supported"); + return MethodNotAllowed(); + } + + if (IsNullOrWhiteSpace(subject)) + { + logger.LogWarning("Subject is null or whitespace"); + ModelState["subject.String"]?.Errors.Clear(); + ModelState.AddModelError("", Messages.SubjectRequired); + } + + if (model == null) + { + logger.LogWarning("Claim data is null"); + ModelState.AddModelError("", Messages.ClaimDataRequired); + } + + if (ModelState.IsValid) + { + logger.LogDebug("Adding claim via service: type={ClaimType}, value={ClaimValue}", model.Type, model.Value); + // ReSharper disable once PossibleNullReferenceException + var result = await service.AddUserClaimAsync(subject, model.Type, model.Value); + if (result.IsSuccess) + { + logger.LogInformation("Successfully added claim {ClaimType} to user {Subject}", model.Type, subject); + return NoContent(); + } + + logger.LogWarning("Failed to add claim. Errors: {Errors}", string.Join(", ", result.Errors ?? new List())); + ModelState.AddErrors(result); + } + + logger.LogWarning("ModelState is invalid when adding claim"); + return BadRequest(ModelState.ToError()); + } + + [HttpDelete] + [Route("{subject}/claims/{type}/{value}", Name = IdentityManagerConstants.RouteNames.RemoveClaim)] + [EndpointName("users-delete-claim")] + [EndpointSummary("TODO.")] + [EndpointDescription("TODO.")] + [Tags(["users"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status204NoContent, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + public async Task RemoveClaimAsync(string subject, string type, string value) + { + logger.LogInformation("RemoveClaimAsync called for subject: {Subject}, type: {Type}", subject, type); + + type = type.FromBase64UrlEncoded(); + value = value.FromBase64UrlEncoded(); + + logger.LogDebug("Decoded claim - type: {Type}, value: {Value}", type, value); + + var meta = await GetMetadataAsync(); + if (!meta.UserMetadata.SupportsClaims) + { + logger.LogWarning("User claims are not supported"); + return MethodNotAllowed(); + } + + if (IsNullOrWhiteSpace(subject) || + IsNullOrWhiteSpace(type) || + IsNullOrWhiteSpace(value)) + { + logger.LogWarning("Subject, type, or value is null or whitespace"); + return NotFound(); + } + + logger.LogDebug("Removing claim via service"); + var result = await service.RemoveUserClaimAsync(subject, type, value); + if (result.IsSuccess) + { + logger.LogInformation("Successfully removed claim {ClaimType} from user {Subject}", type, subject); + return NoContent(); + } + + logger.LogWarning("Failed to remove claim. Errors: {Errors}", string.Join(", ", result.Errors ?? new List())); + return BadRequest(result.ToError()); + } + + [HttpPost] + [Route("{subject}/roles/{role}", Name = IdentityManagerConstants.RouteNames.AddRole)] + [EndpointName("users-add-role")] + [EndpointSummary("TODO.")] + [EndpointDescription("TODO.")] + [Tags(["users"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status204NoContent, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + public async Task AddRoleAsync(string subject, string role) + { + logger.LogInformation("AddRoleAsync called for subject: {Subject}, role: {Role}", subject, role); + + var meta = await GetMetadataAsync(); + if (IsNullOrWhiteSpace(meta.RoleMetadata.RoleClaimType)) + { + logger.LogWarning("Role claim type is not configured"); + return MethodNotAllowed(); + } + + if (IsNullOrWhiteSpace(subject)) + { + logger.LogWarning("Subject is null or whitespace"); + return NotFound(); + } + + role = role.FromBase64UrlEncoded(); + logger.LogDebug("Decoded role: {Role}", role); + + logger.LogDebug("Adding role via claim service"); + var result = await service.AddUserClaimAsync(subject, meta.RoleMetadata.RoleClaimType, role); + if (result.IsSuccess) + { + logger.LogInformation("Successfully added role {Role} to user {Subject}", role, subject); + return NoContent(); + } + + logger.LogWarning("Failed to add role. Errors: {Errors}", string.Join(", ", result.Errors ?? new List())); + return BadRequest(result.ToError()); + } + + [HttpDelete] + [Route("{subject}/roles/{role}", Name = IdentityManagerConstants.RouteNames.RemoveRole)] + [EndpointName("users-delete-role")] + [EndpointSummary("TODO.")] + [EndpointDescription("TODO.")] + [Tags(["users"])] + // [Consumes] + [ProducesResponseType(StatusCodes.Status204NoContent, "application/json")] + [ProducesResponseType(StatusCodes.Status401Unauthorized, "application/problem+json")] + [ProducesResponseType(StatusCodes.Status400BadRequest, "application/problem+json")] + public async Task RemoveRoleAsync(string subject, string role) + { + logger.LogInformation("RemoveRoleAsync called for subject: {Subject}, role: {Role}", subject, role); + + var meta = await GetMetadataAsync(); + if (IsNullOrWhiteSpace(meta.RoleMetadata.RoleClaimType)) + { + logger.LogWarning("Role claim type is not configured"); + return MethodNotAllowed(); + } + + if (IsNullOrWhiteSpace(subject)) + { + logger.LogWarning("Subject is null or whitespace"); + return NotFound(); + } + + role = role.FromBase64UrlEncoded(); + logger.LogDebug("Decoded role: {Role}", role); + + logger.LogDebug("Removing role via claim service"); + var result = await service.RemoveUserClaimAsync(subject, meta.RoleMetadata.RoleClaimType, role); + if (result.IsSuccess) + { + logger.LogInformation("Successfully removed role {Role} from user {Subject}", role, subject); + return NoContent(); + } + + logger.LogWarning("Failed to remove role. Errors: {Errors}", string.Join(", ", result.Errors ?? new List())); + return BadRequest(result.ToError()); + } + + #endregion + + #endregion + + #region Helpers + + [NonAction] + private IEnumerable ValidateCreateProperties(UserMetadata userMetadata, IEnumerable properties) + { + if (userMetadata == null) throw new ArgumentNullException(nameof(userMetadata)); + properties = properties ?? Enumerable.Empty(); + + var meta = userMetadata.GetCreateProperties(); + return meta.Validate(properties); + } + + [NonAction] + private void ValidateUpdateProperty(UserMetadata userMetadata, string type, string value) + { + if (userMetadata == null) throw new ArgumentNullException(nameof(userMetadata)); + + if (IsNullOrWhiteSpace(type)) + { + logger.LogWarning("Property type is required but was not provided"); + ModelState.AddModelError("", Messages.PropertyTypeRequired); + return; + } + + var prop = userMetadata.UpdateProperties.SingleOrDefault(x => x.Type == type); + if (prop == null) + { + logger.LogWarning("Invalid property type: {Type}", type); + ModelState.AddModelError("", Format(Messages.PropertyInvalid, type)); + } + else + { + var error = prop.Validate(value); + if (error != null) + { + logger.LogWarning("Property validation failed for type {Type}: {Error}", type, error); + ModelState.AddModelError("", error); + } + } + } + + [NonAction] + private IActionResult MethodNotAllowed() + { + return StatusCode(405); + } + + #endregion + } +} diff --git a/src/IdentityManager2.Apis/Api/Controllers/_BaseController.cs b/src/IdentityManager2.Apis/Api/Controllers/_BaseController.cs new file mode 100644 index 0000000..4484a94 --- /dev/null +++ b/src/IdentityManager2.Apis/Api/Controllers/_BaseController.cs @@ -0,0 +1,49 @@ +using IdentityManager2.Core.Metadata; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; + +namespace IdentityManager2.Api.Controllers; + +/* +public static class AreaApiConstants +{ + public const string AreaName = "idmgr2"; +} +// */ + +[ApiController] +//[Area(AreaApiConstants.AreaName)] +[Authorize(IdentityManagerConstants.IdMgrAuthPolicy)] +[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] +public abstract class BaseApiController : ControllerBase +{ + protected readonly ILogger logger; + protected readonly IIdentityManagerService service; + protected IdentityManagerMetadata metadata; + + #region Constructors + + protected BaseApiController(IIdentityManagerService service, ILogger logger) + { + this.service = service ?? throw new ArgumentNullException(nameof(service)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + #endregion + + [NonAction] + protected async Task GetMetadataAsync() + { + if (metadata == null) + { + metadata = await service.GetMetadataAsync(); + if (metadata == null) throw new InvalidOperationException("GetMetadataAsync returned null"); + metadata.Validate(); + } + + return metadata; + } +} diff --git a/src/IdentityManager2/Api/Models/CreateRoleLink.cs b/src/IdentityManager2.Apis/Api/Models/CreateRoleLink.cs similarity index 100% rename from src/IdentityManager2/Api/Models/CreateRoleLink.cs rename to src/IdentityManager2.Apis/Api/Models/CreateRoleLink.cs diff --git a/src/IdentityManager2/Api/Models/CreateUserLink.cs b/src/IdentityManager2.Apis/Api/Models/CreateUserLink.cs similarity index 100% rename from src/IdentityManager2/Api/Models/CreateUserLink.cs rename to src/IdentityManager2.Apis/Api/Models/CreateUserLink.cs diff --git a/src/IdentityManager2/Api/Models/ErrorModel.cs b/src/IdentityManager2.Apis/Api/Models/ErrorModel.cs similarity index 100% rename from src/IdentityManager2/Api/Models/ErrorModel.cs rename to src/IdentityManager2.Apis/Api/Models/ErrorModel.cs diff --git a/src/IdentityManager2.Apis/Api/Models/JsonSerializers.cs b/src/IdentityManager2.Apis/Api/Models/JsonSerializers.cs new file mode 100644 index 0000000..3a2a0ba --- /dev/null +++ b/src/IdentityManager2.Apis/Api/Models/JsonSerializers.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using IdentityManager2.Api.Models; +using IdentityManager2.Core; +using IdentityManager2.Core.Metadata; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(PropertyDataType))] +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(AnonymousSubject))] +[JsonSerializable(typeof(AnonymousSubjectRole))] +[JsonSerializable(typeof(AnonymousUserName))] +[JsonSerializable(typeof(AnonymousDetail))] +[JsonSerializable(typeof(UserDetailResource))] +[JsonSerializable(typeof(UserDetailDataResource))] +[JsonSerializable(typeof(UserQueryResultResource))] +[JsonSerializable(typeof(UserQueryResultResourceData))] +[JsonSerializable(typeof(MetaResult))] +public partial class MetaResult_Context : JsonSerializerContext { } + + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(UserQueryResultResource))] +public partial class UserQueryResultResource_Context : JsonSerializerContext { } + + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(UserDetailDataResource))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(AnonymousUpdate))] +[JsonSerializable(typeof(AnonymousClaim))] +[JsonSerializable(typeof(AnonymousRolesDataMetaLink[]))] +[JsonSerializable(typeof(AnonymousRolesActionLinks))] +[JsonSerializable(typeof(UserDetailResource))] +public partial class UserDetailResource_Context : JsonSerializerContext { } + + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(RoleQueryResultResource))] +public partial class RoleQueryResultResource_Context : JsonSerializerContext { } + + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(RoleDetailResource))] +public partial class RoleDetailResource_Context : JsonSerializerContext { } + + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(PropertyValue[]))] +public partial class ArrayPropertyValue_Context : JsonSerializerContext { } + + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(ClaimValue))] +public partial class ClaimValue_Context: JsonSerializerContext { } + + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(AnonymousCreatedRole))] +public partial class AnonymousCreatedRole_Context : JsonSerializerContext { } + + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(AnonymousCreatedUser))] +public partial class AnonymousCreatedUser_Context : JsonSerializerContext { } + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(ModelStateDictionary))] +public partial class ModelStateDictionary_Context : JsonSerializerContext { } + + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(ErrorModel))] +public partial class ErrorModel_Context : JsonSerializerContext { } + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(List))] +public partial class ListStringErrors_Context : JsonSerializerContext { } + + + + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(Microsoft.AspNetCore.Mvc.SerializableError))] +public partial class SerializableError_Context : JsonSerializerContext { } diff --git a/src/IdentityManager2.Apis/Api/Models/PageModel.cs b/src/IdentityManager2.Apis/Api/Models/PageModel.cs new file mode 100644 index 0000000..acd653e --- /dev/null +++ b/src/IdentityManager2.Apis/Api/Models/PageModel.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using IdentityManager2.Core; +using IdentityManager2.Core.Metadata; + +namespace IdentityManager2.Api.Models +{ + public sealed class MetaResult + { + public Dictionary Data { get; set; } + public Dictionary Links { get; set; } + } + + public sealed class AnonymousUserName + { + public string username { get; set; } + } + + public class AnonymousSubject + { + public string subject { get; set; } + } + + public sealed class AnonymousSubjectRole : AnonymousSubject + { + public string role { get; set; } + } + + public sealed class AnonymousDetail + { + public string detail { get; set; } + } + + public sealed class AnonymousUpdate + { + public string update { get; set; } + } + + public sealed class AnonymousTypeDescription + { + public string type { get; set; } + public string description { get; set; } + } + + public sealed class AnonymousPropertiesDataMetaLink + { + public object Data { get; set; } + public PropertyMetadata Meta { get; set; } + public object Links { get; set; } + } + + public sealed class AnonymousRolesDataMetaLink + { + public object data { get; set; } + public AnonymousTypeDescription meta { get; set; } + public object links { get; set; } + } + + public sealed class AnonymousRolesActionLinks + { + public string add { get; set; } + public string remove { get; set; } + } + + public sealed class AnonymousRolesDeleteLink + { + public string delete { get; set; } + } + + public sealed class AnonymousCreateLink + { + public string create { get; set; } + } + + public sealed class AnonymousClaimLinks + { + public ClaimValue Data { get; set; } + public AnonymousRolesDeleteLink Links { get; set; } + } + + public sealed class AnonymousClaim + { + public IEnumerable Data { get; set; } + public AnonymousCreateLink Links { get; set; } + } + + public sealed class AnonymousCreatedRole + { + public AnonymousSubject Data { get; set; } + public AnonymousDetail Links { get; set; } + } + + public sealed class AnonymousCreatedUser + { + public AnonymousSubject Data { get; set; } + public AnonymousDetail Links { get; set; } + }; +} \ No newline at end of file diff --git a/src/IdentityManager2/Api/Models/RoleDetailResource.cs b/src/IdentityManager2.Apis/Api/Models/RoleDetailResource.cs similarity index 100% rename from src/IdentityManager2/Api/Models/RoleDetailResource.cs rename to src/IdentityManager2.Apis/Api/Models/RoleDetailResource.cs diff --git a/src/IdentityManager2/Api/Models/RoleQueryResultResource.cs b/src/IdentityManager2.Apis/Api/Models/RoleQueryResultResource.cs similarity index 100% rename from src/IdentityManager2/Api/Models/RoleQueryResultResource.cs rename to src/IdentityManager2.Apis/Api/Models/RoleQueryResultResource.cs diff --git a/src/IdentityManager2/Api/Models/UserDetailResource.cs b/src/IdentityManager2.Apis/Api/Models/UserDetailResource.cs similarity index 100% rename from src/IdentityManager2/Api/Models/UserDetailResource.cs rename to src/IdentityManager2.Apis/Api/Models/UserDetailResource.cs diff --git a/src/IdentityManager2/Api/Models/UserQueryResultResource.cs b/src/IdentityManager2.Apis/Api/Models/UserQueryResultResource.cs similarity index 100% rename from src/IdentityManager2/Api/Models/UserQueryResultResource.cs rename to src/IdentityManager2.Apis/Api/Models/UserQueryResultResource.cs diff --git a/src/IdentityManager2/Configuration/DependencyInjection/IIdentityManagerBuilder.cs b/src/IdentityManager2.Apis/Configuration/DependencyInjection/IIdentityManagerBuilder.cs similarity index 100% rename from src/IdentityManager2/Configuration/DependencyInjection/IIdentityManagerBuilder.cs rename to src/IdentityManager2.Apis/Configuration/DependencyInjection/IIdentityManagerBuilder.cs diff --git a/src/IdentityManager2.Apis/Configuration/DependencyInjection/IdentityManagerEndpointRouteBuilderExtensions.cs b/src/IdentityManager2.Apis/Configuration/DependencyInjection/IdentityManagerEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000..c35c0c9 --- /dev/null +++ b/src/IdentityManager2.Apis/Configuration/DependencyInjection/IdentityManagerEndpointRouteBuilderExtensions.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Scalar.AspNetCore; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("IdentityManager2")] + +namespace Microsoft.AspNetCore.Builder; + +public static class IdentityManagerEndpointRouteBuilderExtensions +{ + internal const string DefaultApiRoute = "/idmgr2"; + + /// + /// . + /// + /// The . + /// The route to register the endpoint on. Must include the route parameter. + /// An that can be used to further customize the endpoint. + public static IEndpointRouteBuilder MapIdentityManagerApis(this IEndpointRouteBuilder endpoints + , [StringSyntax("Route")] string pattern = DefaultApiRoute) + { + var endpointGroup = endpoints.MapGroup(pattern); + + endpointGroup.MapOpenApi().CacheOutput(); + endpointGroup.MapScalarApiReference(); + + return endpoints; + } +} \ No newline at end of file diff --git a/src/IdentityManager2.Apis/Configuration/DependencyInjection/IdentityManagerServiceCollectionExtensions.cs b/src/IdentityManager2.Apis/Configuration/DependencyInjection/IdentityManagerServiceCollectionExtensions.cs new file mode 100644 index 0000000..a7d25aa --- /dev/null +++ b/src/IdentityManager2.Apis/Configuration/DependencyInjection/IdentityManagerServiceCollectionExtensions.cs @@ -0,0 +1,115 @@ +using IdentityManager2; +using IdentityManager2.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class IdentityManagerServiceCollectionExtensions +{ + [RequiresUnreferencedCode("Contains trimming unsafe calls")] + public static IIdentityManagerBuilder AddIdentityManager(this IServiceCollection services, Action optionsAction = null) + { + var builder = new IdentityManagerBuilder(services); + + services.Configure(optionsAction ?? (options => { })); + + var identityManagerOptions = services.BuildServiceProvider().GetRequiredService>().Value; + identityManagerOptions.Validate(); + + services + .AddControllers() + .AddJsonOptions(static options => + { + options.JsonSerializerOptions.TypeInfoResolverChain.Add(ArrayPropertyValue_Context.Default); + options.JsonSerializerOptions.TypeInfoResolverChain.Add(ClaimValue_Context.Default); + options.JsonSerializerOptions.TypeInfoResolverChain.Add(UserQueryResultResource_Context.Default); + options.JsonSerializerOptions.TypeInfoResolverChain.Add(ErrorModel_Context.Default); + options.JsonSerializerOptions.TypeInfoResolverChain.Add(MetaResult_Context.Default); + options.JsonSerializerOptions.TypeInfoResolverChain.Add(UserDetailResource_Context.Default); + options.JsonSerializerOptions.TypeInfoResolverChain.Add(ListStringErrors_Context.Default); + options.JsonSerializerOptions.TypeInfoResolverChain.Add(ModelStateDictionary_Context.Default); + options.JsonSerializerOptions.TypeInfoResolverChain.Add(AnonymousCreatedUser_Context.Default); + options.JsonSerializerOptions.TypeInfoResolverChain.Add(MetaResult_Context.Default); + options.JsonSerializerOptions.TypeInfoResolverChain.Add(RoleQueryResultResource_Context.Default); + options.JsonSerializerOptions.TypeInfoResolverChain.Add(AnonymousCreatedRole_Context.Default); + options.JsonSerializerOptions.TypeInfoResolverChain.Add(RoleDetailResource_Context.Default); + options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializableError_Context.Default); + }); + + if (!string.IsNullOrEmpty(identityManagerOptions.SecurityConfiguration.AuthenticationScheme)) + { + // IdentityManager API authentication scheme + services.AddAuthentication() + .AddCookie(identityManagerOptions.SecurityConfiguration.AuthenticationScheme, options => + { + options.Cookie.Name = identityManagerOptions.SecurityConfiguration.AuthenticationScheme; + options.Cookie.SameSite = SameSiteMode.Strict; + options.Cookie.HttpOnly = true; + options.Cookie.IsEssential = true; + options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; + + // TODO: API Cookie: SlidingExpiration + // TODO: API Cookie: ExpireTimeSpan + + options.LoginPath = identityManagerOptions.SecurityConfiguration.LoginPath; + options.LogoutPath = identityManagerOptions.SecurityConfiguration.LogoutPath; + + options.Events.OnRedirectToLogin = context => + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + return Task.CompletedTask; + }; + options.Events.OnRedirectToAccessDenied = context => + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + return Task.CompletedTask; + }; + }); + } + + // IdentityManager API authorization scheme + services.AddAuthorization(options => + { + var policy = options.GetPolicy(IdentityManagerConstants.IdMgrAuthPolicy); + if (policy != null) throw new InvalidOperationException($"Authorization policy with name {IdentityManagerConstants.IdMgrAuthPolicy} already exists"); + + options.AddPolicy(IdentityManagerConstants.IdMgrAuthPolicy, config => + { + // IdentityManager role + config.RequireClaim(identityManagerOptions.SecurityConfiguration.RoleClaimType, identityManagerOptions.SecurityConfiguration.AdminRoleName); + + // IdentityManager authentication scheme + if (!string.IsNullOrEmpty(identityManagerOptions.SecurityConfiguration.AuthenticationScheme)) + config.AddAuthenticationSchemes(identityManagerOptions.SecurityConfiguration.AuthenticationScheme); + }); + }); + + if (!string.IsNullOrEmpty(identityManagerOptions.SecurityConfiguration.AuthenticationScheme)) + identityManagerOptions.SecurityConfiguration.Configure(services); + + // OpenAPI Configuration + builder.Services.AddOpenApi(options => + { + options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0; + }); + + return builder; + } + + [RequiresUnreferencedCode("Contains trimming unsafe calls")] + public static IIdentityManagerBuilder AddIdentityMangerService(this IIdentityManagerBuilder builder) + where T : class, IIdentityManagerService + { + builder.Services.AddTransient(); + return builder; + } + + public static IIdentityManagerBuilder AddIdentityManagerBuilder(this IServiceCollection services) + { + return new IdentityManagerBuilder(services); + } +} \ No newline at end of file diff --git a/src/IdentityManager2/Configuration/IdentityManagerOptions.cs b/src/IdentityManager2.Apis/Configuration/IdentityManagerOptions.cs similarity index 100% rename from src/IdentityManager2/Configuration/IdentityManagerOptions.cs rename to src/IdentityManager2.Apis/Configuration/IdentityManagerOptions.cs diff --git a/src/IdentityManager2/Configuration/LocalhostAuthentication/LocalhostAuthenticationHandler.cs b/src/IdentityManager2.Apis/Configuration/LocalhostAuthentication/LocalhostAuthenticationHandler.cs similarity index 98% rename from src/IdentityManager2/Configuration/LocalhostAuthentication/LocalhostAuthenticationHandler.cs rename to src/IdentityManager2.Apis/Configuration/LocalhostAuthentication/LocalhostAuthenticationHandler.cs index 243e8be..90e1c02 100644 --- a/src/IdentityManager2/Configuration/LocalhostAuthentication/LocalhostAuthenticationHandler.cs +++ b/src/IdentityManager2.Apis/Configuration/LocalhostAuthentication/LocalhostAuthenticationHandler.cs @@ -2,7 +2,6 @@ using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; -using IdentityManager2.Resources; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/src/IdentityManager2/Configuration/LocalhostAuthentication/LocalhostAuthenticationOptions.cs b/src/IdentityManager2.Apis/Configuration/LocalhostAuthentication/LocalhostAuthenticationOptions.cs similarity index 100% rename from src/IdentityManager2/Configuration/LocalhostAuthentication/LocalhostAuthenticationOptions.cs rename to src/IdentityManager2.Apis/Configuration/LocalhostAuthentication/LocalhostAuthenticationOptions.cs diff --git a/src/IdentityManager2/Configuration/LocalhostAuthentication/LocalhostSecurityConfiguration.cs b/src/IdentityManager2.Apis/Configuration/LocalhostAuthentication/LocalhostSecurityConfiguration.cs similarity index 100% rename from src/IdentityManager2/Configuration/LocalhostAuthentication/LocalhostSecurityConfiguration.cs rename to src/IdentityManager2.Apis/Configuration/LocalhostAuthentication/LocalhostSecurityConfiguration.cs diff --git a/src/IdentityManager2/Configuration/SecurityConfiguration.cs b/src/IdentityManager2.Apis/Configuration/SecurityConfiguration.cs similarity index 87% rename from src/IdentityManager2/Configuration/SecurityConfiguration.cs rename to src/IdentityManager2.Apis/Configuration/SecurityConfiguration.cs index 5e73f88..dfdf998 100644 --- a/src/IdentityManager2/Configuration/SecurityConfiguration.cs +++ b/src/IdentityManager2.Apis/Configuration/SecurityConfiguration.cs @@ -56,13 +56,5 @@ internal virtual void Validate() public virtual void Configure(IServiceCollection services) { } - - internal virtual async Task SignOut(HttpContext context) - { - await context.SignOutAsync(HostAuthenticationType); - - if (!string.IsNullOrWhiteSpace(AdditionalSignOutType)) - await context.SignOutAsync(AdditionalSignOutType); - } } } \ No newline at end of file diff --git a/src/IdentityManager2/Core/ClaimValue.cs b/src/IdentityManager2.Apis/Core/ClaimValue.cs similarity index 92% rename from src/IdentityManager2/Core/ClaimValue.cs rename to src/IdentityManager2.Apis/Core/ClaimValue.cs index d9f4c61..b281836 100644 --- a/src/IdentityManager2/Core/ClaimValue.cs +++ b/src/IdentityManager2.Apis/Core/ClaimValue.cs @@ -1,5 +1,4 @@ using System.ComponentModel.DataAnnotations; -using IdentityManager2.Resources; namespace IdentityManager2.Core { diff --git a/src/IdentityManager2/Core/CreateResult.cs b/src/IdentityManager2.Apis/Core/CreateResult.cs similarity index 100% rename from src/IdentityManager2/Core/CreateResult.cs rename to src/IdentityManager2.Apis/Core/CreateResult.cs diff --git a/src/IdentityManager2/Core/IIdentityManagerService.cs b/src/IdentityManager2.Apis/Core/IIdentityManagerService.cs similarity index 100% rename from src/IdentityManager2/Core/IIdentityManagerService.cs rename to src/IdentityManager2.Apis/Core/IIdentityManagerService.cs diff --git a/src/IdentityManager2/Core/IdentityManagerResult'.cs b/src/IdentityManager2.Apis/Core/IdentityManagerResult'.cs similarity index 100% rename from src/IdentityManager2/Core/IdentityManagerResult'.cs rename to src/IdentityManager2.Apis/Core/IdentityManagerResult'.cs diff --git a/src/IdentityManager2/Core/IdentityManagerResult.cs b/src/IdentityManager2.Apis/Core/IdentityManagerResult.cs similarity index 100% rename from src/IdentityManager2/Core/IdentityManagerResult.cs rename to src/IdentityManager2.Apis/Core/IdentityManagerResult.cs diff --git a/src/IdentityManager2/Core/Metadata/AsyncExecutablePropertyMetadata.cs b/src/IdentityManager2.Apis/Core/Metadata/AsyncExecutablePropertyMetadata.cs similarity index 100% rename from src/IdentityManager2/Core/Metadata/AsyncExecutablePropertyMetadata.cs rename to src/IdentityManager2.Apis/Core/Metadata/AsyncExecutablePropertyMetadata.cs diff --git a/src/IdentityManager2/Core/Metadata/AsyncExpressionPropertyMetadata.cs b/src/IdentityManager2.Apis/Core/Metadata/AsyncExpressionPropertyMetadata.cs similarity index 98% rename from src/IdentityManager2/Core/Metadata/AsyncExpressionPropertyMetadata.cs rename to src/IdentityManager2.Apis/Core/Metadata/AsyncExpressionPropertyMetadata.cs index c166dde..7135555 100644 --- a/src/IdentityManager2/Core/Metadata/AsyncExpressionPropertyMetadata.cs +++ b/src/IdentityManager2.Apis/Core/Metadata/AsyncExpressionPropertyMetadata.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using IdentityManager2.Extensions; -using IdentityManager2.Resources; namespace IdentityManager2.Core.Metadata { diff --git a/src/IdentityManager2/Core/Metadata/ExecutablePropertyMetadata.cs b/src/IdentityManager2.Apis/Core/Metadata/ExecutablePropertyMetadata.cs similarity index 100% rename from src/IdentityManager2/Core/Metadata/ExecutablePropertyMetadata.cs rename to src/IdentityManager2.Apis/Core/Metadata/ExecutablePropertyMetadata.cs diff --git a/src/IdentityManager2/Core/Metadata/ExpressionPropertyMetadata.cs b/src/IdentityManager2.Apis/Core/Metadata/ExpressionPropertyMetadata.cs similarity index 98% rename from src/IdentityManager2/Core/Metadata/ExpressionPropertyMetadata.cs rename to src/IdentityManager2.Apis/Core/Metadata/ExpressionPropertyMetadata.cs index 975cd3c..1ede5fa 100644 --- a/src/IdentityManager2/Core/Metadata/ExpressionPropertyMetadata.cs +++ b/src/IdentityManager2.Apis/Core/Metadata/ExpressionPropertyMetadata.cs @@ -1,6 +1,5 @@ using System; using IdentityManager2.Extensions; -using IdentityManager2.Resources; namespace IdentityManager2.Core.Metadata { diff --git a/src/IdentityManager2/Core/Metadata/IdentityManagerMetadata.cs b/src/IdentityManager2.Apis/Core/Metadata/IdentityManagerMetadata.cs similarity index 100% rename from src/IdentityManager2/Core/Metadata/IdentityManagerMetadata.cs rename to src/IdentityManager2.Apis/Core/Metadata/IdentityManagerMetadata.cs diff --git a/src/IdentityManager2/Core/Metadata/PropertyDataType.cs b/src/IdentityManager2.Apis/Core/Metadata/PropertyDataType.cs similarity index 100% rename from src/IdentityManager2/Core/Metadata/PropertyDataType.cs rename to src/IdentityManager2.Apis/Core/Metadata/PropertyDataType.cs diff --git a/src/IdentityManager2/Core/Metadata/PropertyMetadata.cs b/src/IdentityManager2.Apis/Core/Metadata/PropertyMetadata.cs similarity index 99% rename from src/IdentityManager2/Core/Metadata/PropertyMetadata.cs rename to src/IdentityManager2.Apis/Core/Metadata/PropertyMetadata.cs index a67672c..91dd89e 100644 --- a/src/IdentityManager2/Core/Metadata/PropertyMetadata.cs +++ b/src/IdentityManager2.Apis/Core/Metadata/PropertyMetadata.cs @@ -7,7 +7,6 @@ using System.Text; using System.Threading.Tasks; using IdentityManager2.Extensions; -using IdentityManager2.Resources; using static System.String; namespace IdentityManager2.Core.Metadata diff --git a/src/IdentityManager2/Core/Metadata/ReflectedPropertyMetadata.cs b/src/IdentityManager2.Apis/Core/Metadata/ReflectedPropertyMetadata.cs similarity index 98% rename from src/IdentityManager2/Core/Metadata/ReflectedPropertyMetadata.cs rename to src/IdentityManager2.Apis/Core/Metadata/ReflectedPropertyMetadata.cs index 437073b..be0dbcf 100644 --- a/src/IdentityManager2/Core/Metadata/ReflectedPropertyMetadata.cs +++ b/src/IdentityManager2.Apis/Core/Metadata/ReflectedPropertyMetadata.cs @@ -1,6 +1,5 @@ using System; using System.Reflection; -using IdentityManager2.Resources; namespace IdentityManager2.Core.Metadata { diff --git a/src/IdentityManager2/Core/Metadata/RoleMetadata.cs b/src/IdentityManager2.Apis/Core/Metadata/RoleMetadata.cs similarity index 100% rename from src/IdentityManager2/Core/Metadata/RoleMetadata.cs rename to src/IdentityManager2.Apis/Core/Metadata/RoleMetadata.cs diff --git a/src/IdentityManager2/Core/Metadata/UserMetadata.cs b/src/IdentityManager2.Apis/Core/Metadata/UserMetadata.cs similarity index 100% rename from src/IdentityManager2/Core/Metadata/UserMetadata.cs rename to src/IdentityManager2.Apis/Core/Metadata/UserMetadata.cs diff --git a/src/IdentityManager2/Core/PropertyValue.cs b/src/IdentityManager2.Apis/Core/PropertyValue.cs similarity index 90% rename from src/IdentityManager2/Core/PropertyValue.cs rename to src/IdentityManager2.Apis/Core/PropertyValue.cs index c05302c..b9b2f2a 100644 --- a/src/IdentityManager2/Core/PropertyValue.cs +++ b/src/IdentityManager2.Apis/Core/PropertyValue.cs @@ -1,5 +1,4 @@ using System.ComponentModel.DataAnnotations; -using IdentityManager2.Resources; namespace IdentityManager2.Core { diff --git a/src/IdentityManager2/Core/QueryResult.cs b/src/IdentityManager2.Apis/Core/QueryResult.cs similarity index 100% rename from src/IdentityManager2/Core/QueryResult.cs rename to src/IdentityManager2.Apis/Core/QueryResult.cs diff --git a/src/IdentityManager2/Core/RoleDetail.cs b/src/IdentityManager2.Apis/Core/RoleDetail.cs similarity index 100% rename from src/IdentityManager2/Core/RoleDetail.cs rename to src/IdentityManager2.Apis/Core/RoleDetail.cs diff --git a/src/IdentityManager2/Core/RoleSummary.cs b/src/IdentityManager2.Apis/Core/RoleSummary.cs similarity index 100% rename from src/IdentityManager2/Core/RoleSummary.cs rename to src/IdentityManager2.Apis/Core/RoleSummary.cs diff --git a/src/IdentityManager2/Core/UserDetail.cs b/src/IdentityManager2.Apis/Core/UserDetail.cs similarity index 100% rename from src/IdentityManager2/Core/UserDetail.cs rename to src/IdentityManager2.Apis/Core/UserDetail.cs diff --git a/src/IdentityManager2/Core/UserSummary.cs b/src/IdentityManager2.Apis/Core/UserSummary.cs similarity index 100% rename from src/IdentityManager2/Core/UserSummary.cs rename to src/IdentityManager2.Apis/Core/UserSummary.cs diff --git a/src/IdentityManager2/Extensions/ClaimsExtensions.cs b/src/IdentityManager2.Apis/Extensions/ClaimsExtensions.cs similarity index 100% rename from src/IdentityManager2/Extensions/ClaimsExtensions.cs rename to src/IdentityManager2.Apis/Extensions/ClaimsExtensions.cs diff --git a/src/IdentityManager2/Extensions/EncodingExtensions.cs b/src/IdentityManager2.Apis/Extensions/EncodingExtensions.cs similarity index 100% rename from src/IdentityManager2/Extensions/EncodingExtensions.cs rename to src/IdentityManager2.Apis/Extensions/EncodingExtensions.cs diff --git a/src/IdentityManager2/Extensions/IdentityManagerResultExtension.cs b/src/IdentityManager2.Apis/Extensions/IdentityManagerResultExtension.cs similarity index 100% rename from src/IdentityManager2/Extensions/IdentityManagerResultExtension.cs rename to src/IdentityManager2.Apis/Extensions/IdentityManagerResultExtension.cs diff --git a/src/IdentityManager2/Extensions/ModelStateDictionaryExtensions.cs b/src/IdentityManager2.Apis/Extensions/ModelStateDictionaryExtensions.cs similarity index 100% rename from src/IdentityManager2/Extensions/ModelStateDictionaryExtensions.cs rename to src/IdentityManager2.Apis/Extensions/ModelStateDictionaryExtensions.cs diff --git a/src/IdentityManager2/Extensions/PropertyInfoExtensions.cs b/src/IdentityManager2.Apis/Extensions/PropertyInfoExtensions.cs similarity index 99% rename from src/IdentityManager2/Extensions/PropertyInfoExtensions.cs rename to src/IdentityManager2.Apis/Extensions/PropertyInfoExtensions.cs index 2837a8f..3324448 100644 --- a/src/IdentityManager2/Extensions/PropertyInfoExtensions.cs +++ b/src/IdentityManager2.Apis/Extensions/PropertyInfoExtensions.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Reflection; using IdentityManager2.Core.Metadata; -using IdentityManager2.Resources; namespace IdentityManager2.Extensions { diff --git a/src/IdentityManager2/Extensions/PropertyMetadataExtensions.cs b/src/IdentityManager2.Apis/Extensions/PropertyMetadataExtensions.cs similarity index 99% rename from src/IdentityManager2/Extensions/PropertyMetadataExtensions.cs rename to src/IdentityManager2.Apis/Extensions/PropertyMetadataExtensions.cs index dc73158..b96887d 100644 --- a/src/IdentityManager2/Extensions/PropertyMetadataExtensions.cs +++ b/src/IdentityManager2.Apis/Extensions/PropertyMetadataExtensions.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using IdentityManager2.Core; using IdentityManager2.Core.Metadata; -using IdentityManager2.Resources; using static System.Boolean; using static System.String; diff --git a/src/IdentityManager2/Extensions/RoleMetadataExtensions.cs b/src/IdentityManager2.Apis/Extensions/RoleMetadataExtensions.cs similarity index 100% rename from src/IdentityManager2/Extensions/RoleMetadataExtensions.cs rename to src/IdentityManager2.Apis/Extensions/RoleMetadataExtensions.cs diff --git a/src/IdentityManager2/Extensions/StreamExtensions.cs b/src/IdentityManager2.Apis/Extensions/StreamExtensions.cs similarity index 100% rename from src/IdentityManager2/Extensions/StreamExtensions.cs rename to src/IdentityManager2.Apis/Extensions/StreamExtensions.cs diff --git a/src/IdentityManager2/Extensions/UserMetadataExtensions.cs b/src/IdentityManager2.Apis/Extensions/UserMetadataExtensions.cs similarity index 95% rename from src/IdentityManager2/Extensions/UserMetadataExtensions.cs rename to src/IdentityManager2.Apis/Extensions/UserMetadataExtensions.cs index b9b84d9..7708b3b 100644 --- a/src/IdentityManager2/Extensions/UserMetadataExtensions.cs +++ b/src/IdentityManager2.Apis/Extensions/UserMetadataExtensions.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using IdentityManager2.Core.Metadata; -using IdentityManager2.Resources; namespace IdentityManager2.Extensions { diff --git a/src/IdentityManager2.Apis/GlobalUsings.cs b/src/IdentityManager2.Apis/GlobalUsings.cs new file mode 100644 index 0000000..3eeb0dd --- /dev/null +++ b/src/IdentityManager2.Apis/GlobalUsings.cs @@ -0,0 +1 @@ +global using IdentityManager2.Apis.Resources; diff --git a/src/IdentityManager2.Apis/IdentityManager2.Apis.csproj b/src/IdentityManager2.Apis/IdentityManager2.Apis.csproj new file mode 100644 index 0000000..a042fdb --- /dev/null +++ b/src/IdentityManager2.Apis/IdentityManager2.Apis.csproj @@ -0,0 +1,56 @@ + + + + net9.0 + IdentityManager is an user management tool for ASP.NET Core. Maintained by Rock Solid Knowledge. + 1.0.2 + Scott Brady + IdentityManager2.Apis + IdentityManager2.Apis + IdentityManager;IdentityManager2;Identity + https://identityserver.github.io/Documentation/assets/images/icons/IDmanager_icon144.jpg + https://github.com/IdentityManager/IdentityManager2 + https://github.com/IdentityManager/IdentityManager2/blob/master/LICENSE + + $(MSBuildProjectDirectory) + true + + + + + + + + + + + + + + + + True + True + ExceptionMessages.resx + + + True + True + Messages.resx + + + + + + PublicResXFileCodeGenerator + ExceptionMessages.Designer.cs + + + PublicResXFileCodeGenerator + Messages.Designer.cs + + + + diff --git a/src/IdentityManager2/IdentityManagerConstants.cs b/src/IdentityManager2.Apis/IdentityManagerConstants.cs similarity index 100% rename from src/IdentityManager2/IdentityManagerConstants.cs rename to src/IdentityManager2.Apis/IdentityManagerConstants.cs diff --git a/src/IdentityManager2/Mappers/RoleResultMappers.cs b/src/IdentityManager2.Apis/Mappers/RoleResultMappers.cs similarity index 100% rename from src/IdentityManager2/Mappers/RoleResultMappers.cs rename to src/IdentityManager2.Apis/Mappers/RoleResultMappers.cs diff --git a/src/IdentityManager2/Mappers/UserResultMappers.cs b/src/IdentityManager2.Apis/Mappers/UserResultMappers.cs similarity index 100% rename from src/IdentityManager2/Mappers/UserResultMappers.cs rename to src/IdentityManager2.Apis/Mappers/UserResultMappers.cs diff --git a/src/IdentityManager2/Resources/ExceptionMessages.Designer.cs b/src/IdentityManager2.Apis/Resources/ExceptionMessages.Designer.cs similarity index 92% rename from src/IdentityManager2/Resources/ExceptionMessages.Designer.cs rename to src/IdentityManager2.Apis/Resources/ExceptionMessages.Designer.cs index cf494d2..4adfdfb 100644 --- a/src/IdentityManager2/Resources/ExceptionMessages.Designer.cs +++ b/src/IdentityManager2.Apis/Resources/ExceptionMessages.Designer.cs @@ -1,81 +1,81 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace IdentityManager2.Resources { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class ExceptionMessages { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal ExceptionMessages() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("IdentityManager2.Resources.ExceptionMessages", typeof(ExceptionMessages).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to {0} is not assigned.. - /// - public static string IsNotAssigned { - get { - return ResourceManager.GetString("IsNotAssigned", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} is null.. - /// - public static string IsNull { - get { - return ResourceManager.GetString("IsNull", resourceCulture); - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace IdentityManager2.Apis.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class ExceptionMessages { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ExceptionMessages() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("IdentityManager2.Apis.Resources.ExceptionMessages", typeof(ExceptionMessages).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to {0} is not assigned.. + /// + public static string IsNotAssigned { + get { + return ResourceManager.GetString("IsNotAssigned", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is null.. + /// + public static string IsNull { + get { + return ResourceManager.GetString("IsNull", resourceCulture); + } + } + } +} diff --git a/src/IdentityManager2/Resources/ExceptionMessages.resx b/src/IdentityManager2.Apis/Resources/ExceptionMessages.resx similarity index 100% rename from src/IdentityManager2/Resources/ExceptionMessages.resx rename to src/IdentityManager2.Apis/Resources/ExceptionMessages.resx diff --git a/src/IdentityManager2/Resources/Messages.Designer.cs b/src/IdentityManager2.Apis/Resources/Messages.Designer.cs similarity index 95% rename from src/IdentityManager2/Resources/Messages.Designer.cs rename to src/IdentityManager2.Apis/Resources/Messages.Designer.cs index acfe355..0c3e5f5 100644 --- a/src/IdentityManager2/Resources/Messages.Designer.cs +++ b/src/IdentityManager2.Apis/Resources/Messages.Designer.cs @@ -1,297 +1,297 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace IdentityManager2.Resources { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class Messages { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Messages() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("IdentityManager2.Resources.Messages", typeof(Messages).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Claim data required.. - /// - public static string ClaimDataRequired { - get { - return ResourceManager.GetString("ClaimDataRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The characters {0} are not allowed as a claim type or value.. - /// - public static string ClaimReservedCharacters { - get { - return ResourceManager.GetString("ClaimReservedCharacters", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Claim type is required.. - /// - public static string ClaimTypeRequired { - get { - return ResourceManager.GetString("ClaimTypeRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Claim value is required.. - /// - public static string ClaimValueRequired { - get { - return ResourceManager.GetString("ClaimValueRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The value failed to be converted.. - /// - public static string ConversionFailed { - get { - return ResourceManager.GetString("ConversionFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Email data required.. - /// - public static string EmailDataRequired { - get { - return ResourceManager.GetString("EmailDataRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Email is required.. - /// - public static string EmailRequired { - get { - return ResourceManager.GetString("EmailRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} is not a valid boolean.. - /// - public static string InvalidBoolean { - get { - return ResourceManager.GetString("InvalidBoolean", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} is not a valid email.. - /// - public static string InvalidEmail { - get { - return ResourceManager.GetString("InvalidEmail", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} is not a valid number.. - /// - public static string InvalidNumber { - get { - return ResourceManager.GetString("InvalidNumber", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} is not a valid URL.. - /// - public static string InvalidUrl { - get { - return ResourceManager.GetString("InvalidUrl", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} is required.. - /// - public static string IsRequired { - get { - return ResourceManager.GetString("IsRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Local User. - /// - public static string LocalUsername { - get { - return ResourceManager.GetString("LocalUsername", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Missing required properties: {0}. - /// - public static string MissingRequiredProperties { - get { - return ResourceManager.GetString("MissingRequiredProperties", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Password. - /// - public static string Password { - get { - return ResourceManager.GetString("Password", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Password data required.. - /// - public static string PasswordDataRequired { - get { - return ResourceManager.GetString("PasswordDataRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Password is required.. - /// - public static string PasswordRequired { - get { - return ResourceManager.GetString("PasswordRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Phone data required.. - /// - public static string PhoneDataRequired { - get { - return ResourceManager.GetString("PhoneDataRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Phone is required.. - /// - public static string PhoneRequired { - get { - return ResourceManager.GetString("PhoneRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} is an invalid property name.. - /// - public static string PropertyInvalid { - get { - return ResourceManager.GetString("PropertyInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Property type is required.. - /// - public static string PropertyTypeRequired { - get { - return ResourceManager.GetString("PropertyTypeRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Subject is required.. - /// - public static string SubjectRequired { - get { - return ResourceManager.GetString("SubjectRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unrecognized properties: {0}. - /// - public static string UnrecognizedProperties { - get { - return ResourceManager.GetString("UnrecognizedProperties", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User data required.. - /// - public static string UserDataRequired { - get { - return ResourceManager.GetString("UserDataRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Username. - /// - public static string Username { - get { - return ResourceManager.GetString("Username", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Username is required.. - /// - public static string UsernameRequired { - get { - return ResourceManager.GetString("UsernameRequired", resourceCulture); - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace IdentityManager2.Apis.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Messages { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Messages() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("IdentityManager2.Apis.Resources.Messages", typeof(Messages).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Claim data required.. + /// + public static string ClaimDataRequired { + get { + return ResourceManager.GetString("ClaimDataRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The characters {0} are not allowed as a claim type or value.. + /// + public static string ClaimReservedCharacters { + get { + return ResourceManager.GetString("ClaimReservedCharacters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Claim type is required.. + /// + public static string ClaimTypeRequired { + get { + return ResourceManager.GetString("ClaimTypeRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Claim value is required.. + /// + public static string ClaimValueRequired { + get { + return ResourceManager.GetString("ClaimValueRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The value failed to be converted.. + /// + public static string ConversionFailed { + get { + return ResourceManager.GetString("ConversionFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Email data required.. + /// + public static string EmailDataRequired { + get { + return ResourceManager.GetString("EmailDataRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Email is required.. + /// + public static string EmailRequired { + get { + return ResourceManager.GetString("EmailRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is not a valid boolean.. + /// + public static string InvalidBoolean { + get { + return ResourceManager.GetString("InvalidBoolean", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is not a valid email.. + /// + public static string InvalidEmail { + get { + return ResourceManager.GetString("InvalidEmail", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is not a valid number.. + /// + public static string InvalidNumber { + get { + return ResourceManager.GetString("InvalidNumber", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is not a valid URL.. + /// + public static string InvalidUrl { + get { + return ResourceManager.GetString("InvalidUrl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is required.. + /// + public static string IsRequired { + get { + return ResourceManager.GetString("IsRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Local User. + /// + public static string LocalUsername { + get { + return ResourceManager.GetString("LocalUsername", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Missing required properties: {0}. + /// + public static string MissingRequiredProperties { + get { + return ResourceManager.GetString("MissingRequiredProperties", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password. + /// + public static string Password { + get { + return ResourceManager.GetString("Password", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password data required.. + /// + public static string PasswordDataRequired { + get { + return ResourceManager.GetString("PasswordDataRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password is required.. + /// + public static string PasswordRequired { + get { + return ResourceManager.GetString("PasswordRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Phone data required.. + /// + public static string PhoneDataRequired { + get { + return ResourceManager.GetString("PhoneDataRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Phone is required.. + /// + public static string PhoneRequired { + get { + return ResourceManager.GetString("PhoneRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is an invalid property name.. + /// + public static string PropertyInvalid { + get { + return ResourceManager.GetString("PropertyInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property type is required.. + /// + public static string PropertyTypeRequired { + get { + return ResourceManager.GetString("PropertyTypeRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Subject is required.. + /// + public static string SubjectRequired { + get { + return ResourceManager.GetString("SubjectRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unrecognized properties: {0}. + /// + public static string UnrecognizedProperties { + get { + return ResourceManager.GetString("UnrecognizedProperties", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User data required.. + /// + public static string UserDataRequired { + get { + return ResourceManager.GetString("UserDataRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Username. + /// + public static string Username { + get { + return ResourceManager.GetString("Username", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Username is required.. + /// + public static string UsernameRequired { + get { + return ResourceManager.GetString("UsernameRequired", resourceCulture); + } + } + } +} diff --git a/src/IdentityManager2/Resources/Messages.resx b/src/IdentityManager2.Apis/Resources/Messages.resx similarity index 100% rename from src/IdentityManager2/Resources/Messages.resx rename to src/IdentityManager2.Apis/Resources/Messages.resx diff --git a/src/IdentityManager2/SecurityHeadersAttribute.cs b/src/IdentityManager2.Apis/SecurityHeadersAttribute.cs similarity index 100% rename from src/IdentityManager2/SecurityHeadersAttribute.cs rename to src/IdentityManager2.Apis/SecurityHeadersAttribute.cs diff --git a/src/IdentityManager2/Api/Controllers/MetaController.cs b/src/IdentityManager2/Api/Controllers/MetaController.cs deleted file mode 100644 index 16dde80..0000000 --- a/src/IdentityManager2/Api/Controllers/MetaController.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using IdentityManager2.Api.Models; -using IdentityManager2.Core.Metadata; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace IdentityManager2.Api.Controllers -{ - [Route(IdentityManagerConstants.MetadataRoutePrefix)] - [Authorize(IdentityManagerConstants.IdMgrAuthPolicy)] - [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] - public class MetaController : Controller - { - private readonly IIdentityManagerService userManager; - private IdentityManagerMetadata metadata; - - public MetaController(IIdentityManagerService userManager) - { - this.userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); - } - - private async Task GetMetadataAsync() - { - if (metadata == null) - { - metadata = await userManager.GetMetadataAsync(); - if (metadata == null) throw new InvalidOperationException("GetMetadataAsync returned null"); - metadata.Validate(); - } - - return metadata; - } - - [Route("")] - public async Task Get() - { - var meta = await GetMetadataAsync(); - var data = new Dictionary { { "currentUser", new AnonymousUserName{ username = User.Identity.Name } } }; - - var links = new Dictionary { ["users"] = Url.Link("GetUsers", null) }; - - if (meta.RoleMetadata.SupportsListing) - { - links["roles"] = Url.Link("GetRoles", null); - } - if (meta.UserMetadata.SupportsCreate) - { - links["createUser"] = new CreateUserLink(Url, meta.UserMetadata); - } - if (meta.RoleMetadata.SupportsCreate) - { - links["createRole"] = new CreateRoleLink(Url, meta.RoleMetadata); - } - - return Ok(new MetaResult - { - Data = data, - Links = links - }); - } - } -} \ No newline at end of file diff --git a/src/IdentityManager2/Api/Controllers/PageController.cs b/src/IdentityManager2/Api/Controllers/PageController.cs index 909e900..d946cf9 100644 --- a/src/IdentityManager2/Api/Controllers/PageController.cs +++ b/src/IdentityManager2/Api/Controllers/PageController.cs @@ -1,88 +1,106 @@ -using System; -using System.Text.Json; -using System.Threading.Tasks; -using IdentityManager2.Api.Models; -using IdentityManager2.Configuration; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace IdentityManager2.Api.Controllers -{ - [SecurityHeaders] - [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] - public class PageController : Controller - { - private readonly IdentityManagerOptions config; - public PageController(IOptions config) - { - this.config = config?.Value ?? throw new ArgumentNullException(nameof(config)); - } - - [HttpGet] - [Route("", Name = IdentityManagerConstants.RouteNames.Home)] - public async Task Index() - { - var authResult = await HttpContext.AuthenticateAsync(config.SecurityConfiguration.HostAuthenticationType); - - return View("/Areas/IdentityManager/Pages/Index.cshtml", new PageModel - { - PathBase = Request.PathBase, - Model = JsonSerializer.Serialize(new PageModelParams - { - PathBase = Request.PathBase, - ShowLoginButton = !authResult.Succeeded, - TitleNavBarLinkTarget = this.config.TitleNavBarLinkTarget, - LoginPath = this.config.SecurityConfiguration.LoginPath, - LogoutPath = this.config.SecurityConfiguration.LogoutPath - }, PageModelParams_Context.Default.PageModelParams) - }); - } - - [HttpGet] - [AllowAnonymous] - [Route("api/login", Name = IdentityManagerConstants.RouteNames.Login)] - public async Task Login() - { - var authResult = await HttpContext.AuthenticateAsync(config.SecurityConfiguration.HostAuthenticationType); - if (authResult.Succeeded) - { - await HttpContext.SignInAsync(IdentityManagerConstants.LocalApiScheme, authResult.Principal); - return RedirectToAction("Index"); - } - - return Challenge(new AuthenticationProperties {RedirectUri = Url.Action("Login")}, config.SecurityConfiguration.HostChallengeType); - } - - [HttpGet] - [AllowAnonymous] - [Route("api/login/refresh")] - public async Task Refresh() - { - var authResult = await HttpContext.AuthenticateAsync(config.SecurityConfiguration.HostAuthenticationType); - if (authResult.Succeeded) - { - await HttpContext.SignInAsync(IdentityManagerConstants.LocalApiScheme, authResult.Principal); - return Ok(); - } - - return Unauthorized(); - } - - [HttpGet] - [AllowAnonymous] - [Route("api/logout", Name = IdentityManagerConstants.RouteNames.Logout)] - public async Task Logout() - { - await HttpContext.SignOutAsync(IdentityManagerConstants.LocalApiScheme); - - await config.SecurityConfiguration.SignOut(HttpContext); - - // if a signout scheme has started a redirect - if (HttpContext.Response.StatusCode == 302) return StatusCode(302); - - return RedirectToRoute(IdentityManagerConstants.RouteNames.Home, null); - } - } +using IdentityManager2.Api.Models; +using IdentityManager2.Configuration; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; +using System; +using System.Text.Json; +using System.Threading.Tasks; + +namespace IdentityManager2.Api.Controllers +{ + [SecurityHeaders] + [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] + public class PageController : Controller + { + private readonly IdentityManagerOptions config; + + public PageController(IOptions config) + { + this.config = config?.Value ?? throw new ArgumentNullException(nameof(config)); + } + + [HttpGet] + [Route("", Name = IdentityManagerConstants.RouteNames.Home)] + public async Task Index() + { + var authResult = await HttpContext.AuthenticateAsync(config.SecurityConfiguration.HostAuthenticationType); + var apiPathBase = Request.PathBase + (Request.Path == "/" ? PathString.Empty : Request.Path); + + return View("/Areas/IdentityManager/Pages/Index.cshtml", new PageModel + { + ApiPathBase = apiPathBase, + PathBase = Request.PathBase, + Model = JsonSerializer.Serialize(new PageModelParams + { + ApiPathBase = apiPathBase, + PathBase = Request.PathBase, + ShowLoginButton = !authResult.Succeeded, + TitleNavBarLinkTarget = this.config.TitleNavBarLinkTarget, + LoginPath = this.config.SecurityConfiguration.LoginPath, + LogoutPath = this.config.SecurityConfiguration.LogoutPath + }, PageModelParams_Context.Default.PageModelParams) + }); + } + + [HttpGet] + [AllowAnonymous] + [Route("api/login", Name = IdentityManagerConstants.RouteNames.Login)] + [EndpointName("api-login")] + public async Task Login() + { + var authResult = await HttpContext.AuthenticateAsync(config.SecurityConfiguration.HostAuthenticationType); + if (authResult.Succeeded) + { + await HttpContext.SignInAsync(IdentityManagerConstants.LocalApiScheme, authResult.Principal); + return RedirectToAction("Index"); + } + + return Challenge(new AuthenticationProperties { RedirectUri = Url.Action("Login") }, config.SecurityConfiguration.HostChallengeType); + } + + [HttpGet] + [AllowAnonymous] + [Route("api/login/refresh")] + [EndpointName("api-refresh")] + public async Task Refresh() + { + var authResult = await HttpContext.AuthenticateAsync(config.SecurityConfiguration.HostAuthenticationType); + if (authResult.Succeeded) + { + await HttpContext.SignInAsync(IdentityManagerConstants.LocalApiScheme, authResult.Principal); + return Ok(); + } + + return Unauthorized(); + } + + [HttpGet] + [AllowAnonymous] + [Route("api/logout", Name = IdentityManagerConstants.RouteNames.Logout)] + [EndpointName("api-logout")] + public async Task Logout() + { + await HttpContext.SignOutAsync(IdentityManagerConstants.LocalApiScheme); + + await this.SignOut(HttpContext); + + // if a signout scheme has started a redirect + if (HttpContext.Response.StatusCode == 302) return StatusCode(302); + + return RedirectToRoute(IdentityManagerConstants.RouteNames.Home, null); + } + + [NonAction] + internal virtual async Task SignOut(HttpContext context) + { + await context.SignOutAsync(config.SecurityConfiguration.HostAuthenticationType); + + if (!string.IsNullOrWhiteSpace(config.SecurityConfiguration.AdditionalSignOutType)) + await context.SignOutAsync(config.SecurityConfiguration.AdditionalSignOutType); + } + } } \ No newline at end of file diff --git a/src/IdentityManager2/Api/Controllers/RolesController.cs b/src/IdentityManager2/Api/Controllers/RolesController.cs deleted file mode 100644 index a7631c9..0000000 --- a/src/IdentityManager2/Api/Controllers/RolesController.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using IdentityManager2.Api.Models; -using IdentityManager2.Core; -using IdentityManager2.Core.Metadata; -using IdentityManager2.Extensions; -using IdentityManager2.Resources; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using static System.String; - -namespace IdentityManager2.Api.Controllers -{ - [Route(IdentityManagerConstants.RoleRoutePrefix)] - [Authorize(IdentityManagerConstants.IdMgrAuthPolicy)] - [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] - public class RolesController : Controller - { - private readonly IIdentityManagerService service; - - public RolesController(IIdentityManagerService service) - { - this.service = service ?? throw new ArgumentNullException(nameof(service)); - } - - private IdentityManagerMetadata metadata; - - public async Task GetMetadataAsync() - { - if (metadata == null) - { - metadata = await service.GetMetadataAsync(); - if (metadata == null) throw new InvalidOperationException("GetMetadataAsync returned null"); - metadata.Validate(); - } - - return metadata; - } - - // GET api/roles - [HttpGet, Route("", Name = IdentityManagerConstants.RouteNames.GetRoles)] - public async Task GetRolesAsync(string filter = null, int start = 0, int count = 100) - { - var meta = await GetMetadataAsync(); - if (!meta.RoleMetadata.SupportsListing) - { - return MethodNotAllowed(); - } - - var result = await service.QueryRolesAsync(filter, start, count); - if (result.IsSuccess) - { - try - { - return Ok(new RoleQueryResultResource(result.Result, Url, meta.RoleMetadata)); - } - catch (Exception exp) - { - throw new ArgumentNullException(exp.ToString()); - } - } - - return BadRequest(result.ToError()); - } - - // POST - [HttpPost, Route("", Name = IdentityManagerConstants.RouteNames.CreateRole)] - public async Task CreateRoleAsync([FromBody] PropertyValue[] properties) - { - var meta = await GetMetadataAsync(); - if (!meta.RoleMetadata.SupportsCreate) - { - return MethodNotAllowed(); - } - - var errors = ValidateCreateProperties(meta.RoleMetadata, properties); - - foreach (var error in errors) - { - ModelState.AddModelError("", error); - } - - if (ModelState.IsValid) - { - var result = await service.CreateRoleAsync(properties); - if (result.IsSuccess) - { - var url = Url.Link(IdentityManagerConstants.RouteNames.GetRole, new AnonymousSubject { subject = result.Result.Subject }); - - var resource = new AnonymousCreatedRole - { - Data = new AnonymousSubject { subject = result.Result.Subject }, - Links = new AnonymousDetail { detail = url } - }; - return Created(url, resource); - } - - ModelState.AddModelError("", errors.ToString()); - } - - return BadRequest(ModelState.ToError()); - } - - [HttpGet("{subject}", Name = IdentityManagerConstants.RouteNames.GetRole)] - public async Task GetRoleAsync(string subject) - { - if (IsNullOrWhiteSpace(subject)) - { - ModelState["subject.String"]?.Errors.Clear(); - ModelState.AddModelError("", Messages.SubjectRequired); - } - - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - var meta = await GetMetadataAsync(); - if (!meta.RoleMetadata.SupportsListing) - { - return MethodNotAllowed(); - } - - var result = await service.GetRoleAsync(subject); - - if (result.IsSuccess) - { - if (result.Result == null) - { - return NotFound(); - } - - var response = Ok(new RoleDetailResource(result.Result, Url, meta.RoleMetadata)); - return response; - } - return BadRequest(result.ToError()); - } - - [HttpDelete, Route("{subject}", Name = IdentityManagerConstants.RouteNames.DeleteRole)] - public async Task DeleteRoleAsync(string subject) - { - var meta = await GetMetadataAsync(); - if (!meta.RoleMetadata.SupportsDelete) - { - return MethodNotAllowed(); - } - - if (!ModelState.IsValid) - { - return BadRequest(ModelState.ToError()); - } - - var result = await service.DeleteRoleAsync(subject); - if (result.IsSuccess) - { - return NoContent(); - } - - return BadRequest(result.ToError()); - } - - [HttpPut, Route("{subject}/properties/{type}", Name = IdentityManagerConstants.RouteNames.UpdateRoleProperty)] - public async Task SetPropertyAsync(string subject, string type) - { - if (IsNullOrWhiteSpace(subject)) - { - ModelState["subject.String"]?.Errors.Clear(); - ModelState.AddModelError("", Messages.SubjectRequired); - } - - type = type.FromBase64UrlEncoded(); - var value = await Request.Body.ReadAsStringAsync(); - - var meta = await GetMetadataAsync(); - - ValidateUpdateProperty(meta.RoleMetadata, type, value); - - if (ModelState.IsValid) - { - var result = await service.SetRolePropertyAsync(subject, type, value); - - if (result.IsSuccess) - { - return NoContent(); - } - - ModelState.AddErrors(result); - } - - return BadRequest(ModelState.ToError()); - } - - private IEnumerable ValidateCreateProperties(RoleMetadata roleMetadata, IEnumerable properties) - { - if (roleMetadata == null) throw new ArgumentNullException(nameof(roleMetadata)); - properties = properties ?? Enumerable.Empty(); - - var meta = roleMetadata.GetCreateProperties(); - return meta.Validate(properties); - } - - private void ValidateUpdateProperty(RoleMetadata roleMetadata, string type, string value) - { - if (roleMetadata == null) throw new ArgumentNullException(nameof(roleMetadata)); - - if (IsNullOrWhiteSpace(type)) - { - ModelState.AddModelError("", Messages.PropertyTypeRequired); - return; - } - - var prop = roleMetadata.UpdateProperties.SingleOrDefault(x => x.Type == type); - if (prop == null) - { - ModelState.AddModelError("", Format(Messages.PropertyInvalid, type)); - } - else - { - var error = prop.Validate(value); - if (error != null) - { - ModelState.AddModelError("", error); - } - } - } - - private IActionResult MethodNotAllowed() - { - return StatusCode(405); - } - } -} diff --git a/src/IdentityManager2/Api/Controllers/UsersController.cs b/src/IdentityManager2/Api/Controllers/UsersController.cs deleted file mode 100644 index 252931c..0000000 --- a/src/IdentityManager2/Api/Controllers/UsersController.cs +++ /dev/null @@ -1,348 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using IdentityManager2.Api.Models; -using IdentityManager2.Core; -using IdentityManager2.Core.Metadata; -using IdentityManager2.Extensions; -using IdentityManager2.Resources; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using static System.String; - -namespace IdentityManager2.Api.Controllers -{ - [Route(IdentityManagerConstants.UserRoutePrefix)] - [Authorize(IdentityManagerConstants.IdMgrAuthPolicy)] - [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] - public class UsersController : Controller - { - private readonly IIdentityManagerService service; - private IdentityManagerMetadata metadata; - - public UsersController(IIdentityManagerService service) - { - this.service = service ?? throw new ArgumentNullException(nameof(service)); - } - - public async Task GetMetadataAsync() - { - if (metadata == null) - { - metadata = await service.GetMetadataAsync(); - if (metadata == null) throw new InvalidOperationException("GetMetadataAsync returned null"); - metadata.Validate(); - } - - return metadata; - } - - [HttpGet, Route("", Name = IdentityManagerConstants.RouteNames.GetUsers)] - public async Task GetUsersAsync(string filter = null, int start = 0, int count = 100) - { - var result = await service.QueryUsersAsync(filter, start, count); - if (result.IsSuccess) - { - var meta = await GetMetadataAsync(); - - var resource = new UserQueryResultResource(result.Result, Url, meta.UserMetadata); - return Ok(resource); - } - - return BadRequest(result.ToError()); - } - - [HttpPost("", Name = IdentityManagerConstants.RouteNames.CreateUser)] - public async Task CreateUserAsync([FromBody] PropertyValue[] properties) - { - var meta = await GetMetadataAsync(); - if (!meta.UserMetadata.SupportsCreate) - { - return MethodNotAllowed(); - } - - var errors = ValidateCreateProperties(meta.UserMetadata, properties); - - foreach (var error in errors) - { - ModelState.AddModelError("", error); - } - - if (ModelState.IsValid) - { - var result = await service.CreateUserAsync(properties); - if (result.IsSuccess) - { - var url = Url.Link(IdentityManagerConstants.RouteNames.GetUser, new AnonymousSubject { subject = result.Result.Subject }); - var resource = new AnonymousCreatedUser - { - Data = new AnonymousSubject { subject = result.Result.Subject }, - Links = new AnonymousDetail { detail = url } - }; - - return Created(url, resource); - } - - ModelState.AddModelError("errors", result.Errors.Aggregate((workingSentence, next) => workingSentence + " " + next)); - if (result.Errors.Count > 0) - return BadRequest(ModelState); - } - - return BadRequest(400); - } - - [HttpGet("{subject}", Name = IdentityManagerConstants.RouteNames.GetUser)] - public async Task GetUserAsync(string subject) - { - if (IsNullOrWhiteSpace(subject)) - { - ModelState["subject.String"]?.Errors.Clear(); - ModelState.AddModelError("", Messages.SubjectRequired); - } - - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - var result = await service.GetUserAsync(subject); - if (result.IsSuccess) - { - if (result.Result == null) - { - return NotFound(); - } - - var meta = await GetMetadataAsync(); - RoleSummary[] roles = null; - if (!IsNullOrWhiteSpace(meta.RoleMetadata.RoleClaimType)) - { - var roleResult = await service.QueryRolesAsync(null, -1, -1); - if (!roleResult.IsSuccess) - { - return BadRequest(roleResult.Errors); - } - - roles = roleResult.Result.Items.ToArray(); - } - - return Ok(new UserDetailResource(result.Result, Url, meta, roles)); - } - - return BadRequest(result.ToError()); - } - - [HttpDelete, Route("{subject}", Name = IdentityManagerConstants.RouteNames.DeleteUser)] - public async Task DeleteUserAsync(string subject) - { - var meta = await GetMetadataAsync(); - if (!meta.UserMetadata.SupportsDelete) - { - return MethodNotAllowed(); - } - - if (IsNullOrWhiteSpace(subject)) - { - ModelState["subject.String"]?.Errors.Clear(); - ModelState.AddModelError("", Messages.SubjectRequired); - } - - if (!ModelState.IsValid) - { - return BadRequest(ModelState.ToError()); - } - - var result = await service.DeleteUserAsync(subject); - if (result.IsSuccess) - { - return NoContent(); - } - - return BadRequest(result.ToError()); - } - - [HttpPut, Route("{subject}/properties/{type}", Name = IdentityManagerConstants.RouteNames.UpdateUserProperty)] - public async Task SetPropertyAsync(string subject, string type) - { - if (IsNullOrWhiteSpace(subject)) - { - ModelState["subject.String"]?.Errors.Clear(); - ModelState.AddModelError("", Messages.SubjectRequired); - } - - type = type.FromBase64UrlEncoded(); - - var value = await Request.Body.ReadAsStringAsync(); - - var meta = await GetMetadataAsync(); - ValidateUpdateProperty(meta.UserMetadata, type, value); - - if (ModelState.IsValid) - { - var result = await service.SetUserPropertyAsync(subject, type, value); - if (result.IsSuccess) - { - return NoContent(); - } - - ModelState.AddErrors(result); - } - - return BadRequest(ModelState.ToError()); - } - - [HttpPost, Route("{subject}/claims", Name = IdentityManagerConstants.RouteNames.AddClaim)] - public async Task AddClaimAsync(string subject, [FromBody] ClaimValue model) - { - var meta = await GetMetadataAsync(); - if (!meta.UserMetadata.SupportsClaims) - { - return MethodNotAllowed(); - } - - if (IsNullOrWhiteSpace(subject)) - { - ModelState["subject.String"]?.Errors.Clear(); - ModelState.AddModelError("", Messages.SubjectRequired); - } - - if (model == null) - { - ModelState.AddModelError("", Messages.ClaimDataRequired); - } - - if (ModelState.IsValid) - { - // ReSharper disable once PossibleNullReferenceException - var result = await service.AddUserClaimAsync(subject, model.Type, model.Value); - if (result.IsSuccess) - { - return NoContent(); - } - - ModelState.AddErrors(result); - } - - return BadRequest(ModelState.ToError()); - } - - [HttpDelete, Route("{subject}/claims/{type}/{value}", Name = IdentityManagerConstants.RouteNames.RemoveClaim)] - public async Task RemoveClaimAsync(string subject, string type, string value) - { - type = type.FromBase64UrlEncoded(); - value = value.FromBase64UrlEncoded(); - - var meta = await GetMetadataAsync(); - if (!meta.UserMetadata.SupportsClaims) - { - return MethodNotAllowed(); - } - - if (IsNullOrWhiteSpace(subject) || - IsNullOrWhiteSpace(type) || - IsNullOrWhiteSpace(value)) - { - return NotFound(); - } - - var result = await service.RemoveUserClaimAsync(subject, type, value); - if (result.IsSuccess) - { - return NoContent(); - } - - return BadRequest(result.ToError()); - } - - [HttpPost, Route("{subject}/roles/{role}", Name = IdentityManagerConstants.RouteNames.AddRole)] - public async Task AddRoleAsync(string subject, string role) - { - var meta = await GetMetadataAsync(); - if (IsNullOrWhiteSpace(meta.RoleMetadata.RoleClaimType)) - { - return MethodNotAllowed(); - } - - if (IsNullOrWhiteSpace(subject)) - { - return NotFound(); - } - - role = role.FromBase64UrlEncoded(); - - var result = await service.AddUserClaimAsync(subject, meta.RoleMetadata.RoleClaimType, role); - if (result.IsSuccess) - { - return NoContent(); - } - - return BadRequest(result.ToError()); - } - - [HttpDelete, Route("{subject}/roles/{role}", Name = IdentityManagerConstants.RouteNames.RemoveRole)] - public async Task RemoveRoleAsync(string subject, string role) - { - var meta = await GetMetadataAsync(); - if (IsNullOrWhiteSpace(meta.RoleMetadata.RoleClaimType)) - { - return MethodNotAllowed(); - } - - if (IsNullOrWhiteSpace(subject)) - { - return NotFound(); - } - - role = role.FromBase64UrlEncoded(); - - var result = await service.RemoveUserClaimAsync(subject, meta.RoleMetadata.RoleClaimType, role); - if (result.IsSuccess) - { - return NoContent(); - } - - return BadRequest(result.ToError()); - } - - private IEnumerable ValidateCreateProperties(UserMetadata userMetadata, IEnumerable properties) - { - if (userMetadata == null) throw new ArgumentNullException(nameof(userMetadata)); - properties = properties ?? Enumerable.Empty(); - - var meta = userMetadata.GetCreateProperties(); - return meta.Validate(properties); - } - - private void ValidateUpdateProperty(UserMetadata userMetadata, string type, string value) - { - if (userMetadata == null) throw new ArgumentNullException(nameof(userMetadata)); - - if (IsNullOrWhiteSpace(type)) - { - ModelState.AddModelError("", Messages.PropertyTypeRequired); - return; - } - - var prop = userMetadata.UpdateProperties.SingleOrDefault(x => x.Type == type); - if (prop == null) - { - ModelState.AddModelError("", Format(Messages.PropertyInvalid, type)); - } - else - { - var error = prop.Validate(value); - if (error != null) - { - ModelState.AddModelError("", error); - } - } - } - - private IActionResult MethodNotAllowed() - { - return StatusCode(405); - } - } -} diff --git a/src/IdentityManager2/Api/Models/JsonSerializers.cs b/src/IdentityManager2/Api/Models/JsonSerializers.cs index c803e40..c2d4a4a 100644 --- a/src/IdentityManager2/Api/Models/JsonSerializers.cs +++ b/src/IdentityManager2/Api/Models/JsonSerializers.cs @@ -8,90 +8,3 @@ [JsonSerializable(typeof(PageModelParams))] partial class PageModelParams_Context : JsonSerializerContext { } - - -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(IEnumerable))] -[JsonSerializable(typeof(IEnumerable))] -[JsonSerializable(typeof(PropertyDataType))] -[JsonSerializable(typeof(string))] -[JsonSerializable(typeof(AnonymousSubject))] -[JsonSerializable(typeof(AnonymousSubjectRole))] -[JsonSerializable(typeof(AnonymousUserName))] -[JsonSerializable(typeof(AnonymousDetail))] -[JsonSerializable(typeof(UserDetailResource))] -[JsonSerializable(typeof(UserDetailDataResource))] -[JsonSerializable(typeof(UserQueryResultResource))] -[JsonSerializable(typeof(UserQueryResultResourceData))] -[JsonSerializable(typeof(MetaResult))] -public partial class MetaResult_Context : JsonSerializerContext { } - - -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(Dictionary))] -[JsonSerializable(typeof(UserQueryResultResource))] -public partial class UserQueryResultResource_Context : JsonSerializerContext { } - - -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(Dictionary))] -[JsonSerializable(typeof(UserDetailDataResource))] -[JsonSerializable(typeof(IEnumerable))] -[JsonSerializable(typeof(AnonymousUpdate))] -[JsonSerializable(typeof(AnonymousClaim))] -[JsonSerializable(typeof(AnonymousRolesDataMetaLink[]))] -[JsonSerializable(typeof(AnonymousRolesActionLinks))] -[JsonSerializable(typeof(UserDetailResource))] -public partial class UserDetailResource_Context : JsonSerializerContext { } - - -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(Dictionary))] -[JsonSerializable(typeof(RoleQueryResultResource))] -public partial class RoleQueryResultResource_Context : JsonSerializerContext { } - - -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(Dictionary))] -[JsonSerializable(typeof(RoleDetailResource))] -public partial class RoleDetailResource_Context : JsonSerializerContext { } - - -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(PropertyValue[]))] -public partial class ArrayPropertyValue_Context : JsonSerializerContext { } - - -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(ClaimValue))] -public partial class ClaimValue_Context: JsonSerializerContext { } - - -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(AnonymousCreatedRole))] -public partial class AnonymousCreatedRole_Context : JsonSerializerContext { } - - -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(AnonymousCreatedUser))] -public partial class AnonymousCreatedUser_Context : JsonSerializerContext { } - -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(ModelStateDictionary))] -public partial class ModelStateDictionary_Context : JsonSerializerContext { } - - -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(ErrorModel))] -public partial class ErrorModel_Context : JsonSerializerContext { } - -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(List))] -public partial class ListStringErrors_Context : JsonSerializerContext { } - - - - -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(Microsoft.AspNetCore.Mvc.SerializableError))] -public partial class SerializableError_Context : JsonSerializerContext { } diff --git a/src/IdentityManager2/Api/Models/PageModel.cs b/src/IdentityManager2/Api/Models/PageModel.cs index 952f012..71aa6a1 100644 --- a/src/IdentityManager2/Api/Models/PageModel.cs +++ b/src/IdentityManager2/Api/Models/PageModel.cs @@ -1,114 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; -using IdentityManager2.Core; -using IdentityManager2.Core.Metadata; +namespace IdentityManager2.Api.Models; -namespace IdentityManager2.Api.Models +public class PageModel { - public class PageModel - { - public string PathBase { get; set; } - public string Model { get; set; } - } + public string ApiPathBase { get; set; } + public string PathBase { get; set; } + public string Model { get; set; } +} - class PageModelParams - { - public string PathBase { get; set; } - public bool ShowLoginButton { get; set; } - public string TitleNavBarLinkTarget { get; set; } - public string LoginPath { get; set; } - public string LogoutPath { get; set; } - } - - public sealed class MetaResult - { - public Dictionary Data { get; set; } - public Dictionary Links { get; set; } - } - - public sealed class AnonymousUserName - { - public string username { get; set; } - } - - public class AnonymousSubject - { - public string subject { get; set; } - } - - public sealed class AnonymousSubjectRole : AnonymousSubject - { - public string role { get; set; } - } - - public sealed class AnonymousDetail - { - public string detail { get; set; } - } - - public sealed class AnonymousUpdate - { - public string update { get; set; } - } - - public sealed class AnonymousTypeDescription - { - public string type { get; set; } - public string description { get; set; } - } - - public sealed class AnonymousPropertiesDataMetaLink - { - public object Data { get; set; } - public PropertyMetadata Meta { get; set; } - public object Links { get; set; } - } - - public sealed class AnonymousRolesDataMetaLink - { - public object data { get; set; } - public AnonymousTypeDescription meta { get; set; } - public object links { get; set; } - } - - public sealed class AnonymousRolesActionLinks - { - public string add { get; set; } - public string remove { get; set; } - } - - public sealed class AnonymousRolesDeleteLink - { - public string delete { get; set; } - } - - public sealed class AnonymousCreateLink - { - public string create { get; set; } - } - - public sealed class AnonymousClaimLinks - { - public ClaimValue Data { get; set; } - public AnonymousRolesDeleteLink Links { get; set; } - } - - public sealed class AnonymousClaim - { - public IEnumerable Data { get; set; } - public AnonymousCreateLink Links { get; set; } - } - - public sealed class AnonymousCreatedRole - { - public AnonymousSubject Data { get; set; } - public AnonymousDetail Links { get; set; } - } - - public sealed class AnonymousCreatedUser - { - public AnonymousSubject Data { get; set; } - public AnonymousDetail Links { get; set; } - }; +class PageModelParams +{ + public string ApiPathBase { get; set; } + public string PathBase { get; set; } + public bool ShowLoginButton { get; set; } + public string TitleNavBarLinkTarget { get; set; } + public string LoginPath { get; set; } + public string LogoutPath { get; set; } } \ No newline at end of file diff --git a/src/IdentityManager2/Assets/Scripts/Bundle.js b/src/IdentityManager2/Assets/Scripts/Bundle.js index 44c1a41..1db77e0 100644 --- a/src/IdentityManager2/Assets/Scripts/Bundle.js +++ b/src/IdentityManager2/Assets/Scripts/Bundle.js @@ -282,7 +282,7 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro const app = angular.module("ttIdm", []); function config($httpProvider) { - function intercept($q, $injector, idmErrorService, PathBase, $rootScope) { + function intercept($q, $injector, idmErrorService, PathBase, ApiPathBase, $rootScope) { var inprogressRefreshRequest = null; return { @@ -335,7 +335,7 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro }; } - intercept.$inject = ["$q", "$injector", "idmErrorService", "PathBase", "$rootScope"]; + intercept.$inject = ["$q", "$injector", "idmErrorService", "PathBase", "ApiPathBase", "$rootScope"]; $httpProvider.interceptors.push(intercept); } config.$inject = ["$httpProvider"]; @@ -364,7 +364,7 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro idmErrorService.$inject = ["$rootScope", "$timeout"]; app.factory("idmErrorService", idmErrorService); - function idmApi($http, $q, PathBase) { + function idmApi($http, $q, ApiPathBase, ApiPathBase) { var cache = null; return { @@ -375,7 +375,7 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro return d.promise; } - return $http.get(PathBase + "/api").then(function(resp) { + return $http.get(ApiPathBase + "/api").then(function(resp) { cache = resp.data; return cache; }, @@ -392,7 +392,7 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro }; } - idmApi.$inject = ["$http", "$q", "PathBase"]; + idmApi.$inject = ["$http", "$q", "PathBase", "ApiPathBase"]; app.factory("idmApi", idmApi); function idmUsers($http, idmApi, $log) { @@ -662,7 +662,7 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro } }; } - ttPropertyEditor.$inject = ["PathBase"]; + ttPropertyEditor.$inject = ["PathBase", "ApiPathBase"]; app.directive("ttPropertyEditor", ttPropertyEditor); function ttPrompt(PathBase) { @@ -683,7 +683,7 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro } }; } - ttPrompt.$inject = ["PathBase"]; + ttPrompt.$inject = ["PathBase", "ApiPathBase"]; app.directive("ttPrompt", ttPrompt); function ttPagerButtons(PathBase) { @@ -696,7 +696,7 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro } }; } - ttPagerButtons.$inject = ["PathBase"]; + ttPagerButtons.$inject = ["PathBase", "ApiPathBase"]; app.directive("ttPagerButtons", ttPagerButtons); function ttPagerSummary(PathBase) { @@ -708,7 +708,7 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro } }; } - ttPagerSummary.$inject = ["PathBase"]; + ttPagerSummary.$inject = ["PathBase", "ApiPathBase"]; app.directive("ttPagerSummary", ttPagerSummary); function idmPager($sce) { @@ -809,7 +809,7 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro } }; } - idmMessage.$inject = ["PathBase"]; + idmMessage.$inject = ["PathBase", "ApiPathBase"]; app.directive("idmMessage", idmMessage); function idmPreventDefault() { @@ -854,7 +854,7 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro templateUrl: PathBase + '/assets/Templates.users.edit.html' }); } - config.$inject = ["$routeProvider", "PathBase"]; + config.$inject = ["$routeProvider", "PathBase", "ApiPathBase"]; app.config(config); function ListUsersCtrl($scope, idmUsers, idmPager, $routeParams, $location) { @@ -1038,7 +1038,7 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro templateUrl: PathBase + '/assets/Templates.roles.edit.html' }); } - config.$inject = ["$routeProvider", "PathBase"]; + config.$inject = ["$routeProvider", "PathBase", "ApiPathBase"]; app.config(config); function ListRolesCtrl($scope, idmRoles, idmPager, $routeParams, $location) { @@ -1158,7 +1158,7 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro (function (angular) { const app = angular.module("ttIdmApp", ["ngRoute", "ttIdm", "ttIdmUI", "ttIdmUsers", "ttIdmRoles"]); - function config(PathBase, $routeProvider) { + function config(PathBase, ApiPathBase, $routeProvider) { $routeProvider .when("/", { templateUrl: PathBase + "/assets/Templates.home.html" @@ -1170,10 +1170,10 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro redirectTo: "/" }); } - config.$inject = ["PathBase", "$routeProvider"]; + config.$inject = ["PathBase", "ApiPathBase", "$routeProvider"]; app.config(config); - function LayoutCtrl($rootScope, PathBase, idmApi, $location, $window, idmErrorService, ShowLoginButton, + function LayoutCtrl($rootScope, PathBase, ApiPathBase, idmApi, $location, $window, idmErrorService, ShowLoginButton, TitleNavBarLinkTarget, LoginPath, LogoutPath) { $rootScope.PathBase = PathBase; $rootScope.layout = {}; @@ -1207,16 +1207,16 @@ p.directive("ngView",v);p.directive("ngView",A);v.$inject=["$route","$anchorScro $rootScope.login = function () { idmErrorService.clear(); - $window.location = PathBase + (LoginPath || "/api/login"); + $window.location = ApiPathBase + (LoginPath || "/api/login"); }; $rootScope.logout = function() { idmErrorService.clear(); - $window.location = PathBase + (LogoutPath || "/api/logout"); + $window.location = ApiPathBase + (LogoutPath || "/api/logout"); }; } - LayoutCtrl.$inject = ["$rootScope", "PathBase", "idmApi", "$location", "$window", "idmErrorService", "ShowLoginButton", + LayoutCtrl.$inject = ["$rootScope", "PathBase", "ApiPathBase", "idmApi", "$location", "$window", "idmErrorService", "ShowLoginButton", "TitleNavBarLinkTarget", "LoginPath", "LogoutPath"]; app.controller("LayoutCtrl", LayoutCtrl); })(angular); diff --git a/src/IdentityManager2/Configuration/DependencyInjection/IdentityManagerServiceCollectionExtensions.cs b/src/IdentityManager2/Configuration/DependencyInjection/IdentityManagerServiceCollectionExtensions.cs deleted file mode 100644 index e02a38d..0000000 --- a/src/IdentityManager2/Configuration/DependencyInjection/IdentityManagerServiceCollectionExtensions.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading.Tasks; -using IdentityManager2; -using IdentityManager2.Configuration; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class IdentityManagerServiceCollectionExtensions - { - [RequiresUnreferencedCode("Contains trimming unsafe calls")] - public static IIdentityManagerBuilder AddIdentityManager(this IServiceCollection services, Action optionsAction = null) - { - services.Configure(optionsAction ?? (options => { })); - - var identityManagerOptions = services.BuildServiceProvider().GetRequiredService>().Value; - identityManagerOptions.Validate(); - - services.AddControllersWithViews() - .AddJsonOptions(static options => - { - options.JsonSerializerOptions.TypeInfoResolverChain.Add(ArrayPropertyValue_Context.Default); - options.JsonSerializerOptions.TypeInfoResolverChain.Add(ClaimValue_Context.Default); - options.JsonSerializerOptions.TypeInfoResolverChain.Add(UserQueryResultResource_Context.Default); - options.JsonSerializerOptions.TypeInfoResolverChain.Add(ErrorModel_Context.Default); - options.JsonSerializerOptions.TypeInfoResolverChain.Add(MetaResult_Context.Default); - options.JsonSerializerOptions.TypeInfoResolverChain.Add(UserDetailResource_Context.Default); - options.JsonSerializerOptions.TypeInfoResolverChain.Add(ListStringErrors_Context.Default); - options.JsonSerializerOptions.TypeInfoResolverChain.Add(ModelStateDictionary_Context.Default); - options.JsonSerializerOptions.TypeInfoResolverChain.Add(AnonymousCreatedUser_Context.Default); - options.JsonSerializerOptions.TypeInfoResolverChain.Add(MetaResult_Context.Default); - options.JsonSerializerOptions.TypeInfoResolverChain.Add(RoleQueryResultResource_Context.Default); - options.JsonSerializerOptions.TypeInfoResolverChain.Add(AnonymousCreatedRole_Context.Default); - options.JsonSerializerOptions.TypeInfoResolverChain.Add(RoleDetailResource_Context.Default); - options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializableError_Context.Default); - }); - - if (!string.IsNullOrEmpty(identityManagerOptions.SecurityConfiguration.AuthenticationScheme)) - { - // IdentityManager API authentication scheme - services.AddAuthentication() - .AddCookie(identityManagerOptions.SecurityConfiguration.AuthenticationScheme, options => - { - options.Cookie.Name = identityManagerOptions.SecurityConfiguration.AuthenticationScheme; - options.Cookie.SameSite = SameSiteMode.Strict; - options.Cookie.HttpOnly = true; - options.Cookie.IsEssential = true; - options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; - - // TODO: API Cookie: SlidingExpiration - // TODO: API Cookie: ExpireTimeSpan - - options.LoginPath = identityManagerOptions.SecurityConfiguration.LoginPath; - options.LogoutPath = identityManagerOptions.SecurityConfiguration.LogoutPath; - - options.Events.OnRedirectToLogin = context => - { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; - return Task.CompletedTask; - }; - options.Events.OnRedirectToAccessDenied = context => - { - context.Response.StatusCode = StatusCodes.Status403Forbidden; - return Task.CompletedTask; - }; - }); - } - - // IdentityManager API authorization scheme - services.AddAuthorization(options => - { - var policy = options.GetPolicy(IdentityManagerConstants.IdMgrAuthPolicy); - if (policy != null) throw new InvalidOperationException($"Authorization policy with name {IdentityManagerConstants.IdMgrAuthPolicy} already exists"); - - options.AddPolicy(IdentityManagerConstants.IdMgrAuthPolicy, config => - { - // IdentityManager role - config.RequireClaim(identityManagerOptions.SecurityConfiguration.RoleClaimType, identityManagerOptions.SecurityConfiguration.AdminRoleName); - - // IdentityManager authentication scheme - if (!string.IsNullOrEmpty(identityManagerOptions.SecurityConfiguration.AuthenticationScheme)) - config.AddAuthenticationSchemes(identityManagerOptions.SecurityConfiguration.AuthenticationScheme); - }); - }); - - if (!string.IsNullOrEmpty(identityManagerOptions.SecurityConfiguration.AuthenticationScheme)) - identityManagerOptions.SecurityConfiguration.Configure(services); - - return new IdentityManagerBuilder(services); - } - - [RequiresUnreferencedCode("Contains trimming unsafe calls")] - public static IIdentityManagerBuilder AddIdentityMangerService(this IIdentityManagerBuilder builder) - where T : class, IIdentityManagerService - { - builder.Services.AddTransient(); - return builder; - } - - public static IIdentityManagerBuilder AddIdentityManagerBuilder(this IServiceCollection services) - { - return new IdentityManagerBuilder(services); - } - } -} \ No newline at end of file diff --git a/src/IdentityManager2/Configuration/Hosting/IdentityManagerApplicationBuilderExtensions.cs b/src/IdentityManager2/Configuration/Hosting/IdentityManagerApplicationBuilderExtensions.cs index 2d93e1c..9fd0198 100644 --- a/src/IdentityManager2/Configuration/Hosting/IdentityManagerApplicationBuilderExtensions.cs +++ b/src/IdentityManager2/Configuration/Hosting/IdentityManagerApplicationBuilderExtensions.cs @@ -1,20 +1,50 @@ -using IdentityManager2.Assets; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.FileProviders; - -namespace Microsoft.AspNetCore.Builder -{ - public static class IdentityManagerApplicationBuilderExtensions - { - public static IApplicationBuilder UseIdentityManager(this IApplicationBuilder app) - { - app.UseFileServer(new FileServerOptions - { - RequestPath = new PathString("/assets"), - FileProvider = new EmbeddedFileProvider(typeof(EmbeddedHtmlResult).Assembly, "IdentityManager2.Assets") - }); - - return app; - } - } +using IdentityManager2.Assets; +using IdentityManager2.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.FileSystemGlobbing.Internal; +using Scalar.AspNetCore; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNetCore.Builder; + +public static class IdentityManagerApplicationBuilderExtensions +{ + internal const string DefaultRoute = IdentityManagerEndpointRouteBuilderExtensions.DefaultApiRoute; + + public static IIdentityManagerBuilder AddIdentityManagerUI(this IIdentityManagerBuilder builder) + { + builder.Services + .AddControllersWithViews() + .AddJsonOptions(static options => + { + // TODO + // options.JsonSerializerOptions.TypeInfoResolverChain.Add(PageModelParams_Context.Default); + }); + + return builder; + } + + public static IEndpointRouteBuilder MapIdentityManagerUI(this IEndpointRouteBuilder endpoints + , [StringSyntax("Route")] string pattern = DefaultRoute) + { + var endpointGroup = endpoints.MapGroup(pattern); + + endpoints.MapIdentityManagerApis(pattern); // do not use endpointGroup here + + return endpoints; + } + + public static IApplicationBuilder UseIdentityManager(this IApplicationBuilder app) + { + app.UseFileServer(new FileServerOptions + { + RequestPath = new PathString("/assets"), + FileProvider = new EmbeddedFileProvider(typeof(EmbeddedHtmlResult).Assembly, "IdentityManager2.Assets") + }); + + return app; + } } \ No newline at end of file diff --git a/src/IdentityManager2/IdentityManager2.csproj b/src/IdentityManager2/IdentityManager2.csproj index e31e672..ac609c2 100644 --- a/src/IdentityManager2/IdentityManager2.csproj +++ b/src/IdentityManager2/IdentityManager2.csproj @@ -1,67 +1,40 @@ - - - - net9.0 - true - IdentityManager is an user management tool for ASP.NET Core. Maintained by Rock Solid Knowledge. - 1.0.1 - Scott Brady - IdentityManager2 - IdentityManager2 - IdentityManager;IdentityManager2;Identity - https://identityserver.github.io/Documentation/assets/images/icons/IDmanager_icon144.jpg - https://github.com/IdentityManager/IdentityManager2 - https://github.com/IdentityManager/IdentityManager2/blob/master/LICENSE - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - ExceptionMessages.resx - - - True - True - Messages.resx - - - - - - PublicResXFileCodeGenerator - ExceptionMessages.Designer.cs - - - PublicResXFileCodeGenerator - Messages.Designer.cs - - - - + + + + net9.0 + true + IdentityManager is an user management tool for ASP.NET Core. Maintained by Rock Solid Knowledge. + 1.0.2 + Scott Brady + IdentityManager2 + IdentityManager2 + IdentityManager;IdentityManager2;Identity + https://identityserver.github.io/Documentation/assets/images/icons/IDmanager_icon144.jpg + https://github.com/IdentityManager/IdentityManager2 + https://github.com/IdentityManager/IdentityManager2/blob/master/LICENSE + + + + + + + + + + + + + + + + + + + + + + + + +