diff --git a/README.md b/README.md index b63d522..09cee4b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -[![build](https://github.com/thomasduft/openiddict-ui/workflows/build/badge.svg)](https://github.com/thomasduft/openiddict-ui/actions) [![NuGet Release](https://img.shields.io/nuget/vpre/tomware.OpenIddict.UI.Api.svg)](https://www.nuget.org/packages/tomware.OpenIddict.UI.Api) + + +| Type | Description | Badge | +| :-------------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- | +| Build | Build status | [![build](https://github.com/thomasduft/openiddict-ui/workflows/build/badge.svg)](https://github.com/thomasduft/openiddict-ui/actions) | +| OpenIddict-UI API | API's for managing OpentIddict `Scopes` and `Applications`. | [![NuGet Release](https://img.shields.io/nuget/vpre/tomware.OpenIddict.UI.Api.svg)](https://www.nuget.org/packages/tomware.OpenIddict.UI.Api) | +| OpentIddict-UI Identity API | API's for managing [ASP.NET Core Identity](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-5.0&tabs=visual-studio) types (Accounts, Roles, etc.). | [![NuGet Release](https://img.shields.io/nuget/vpre/tomware.OpenIddict.UI.Identity.Api.svg)](https://www.nuget.org/packages/tomware.OpenIddict.UI.Identity.Api) | # OpenIddict UI diff --git a/notes.txt b/notes.txt index b89f68a..00d5284 100644 --- a/notes.txt +++ b/notes.txt @@ -1,7 +1,15 @@ TODO: +- organize APIs a bit more by feature + - i.e. identity -> AccountController + - AccountController + - RegisterUserController + - ChangePasswordController + + - extract logic in AuthorizationController to service + - structuring - suite (openiddict-ui-suite) - Core diff --git a/samples/Server/Services/MigrationService.cs b/samples/Server/Services/MigrationService.cs index 04ec07b..502f3a5 100644 --- a/samples/Server/Services/MigrationService.cs +++ b/samples/Server/Services/MigrationService.cs @@ -44,30 +44,31 @@ public async Task EnsureMigrationAsync() await EnsureAdministratorRole(scope.ServiceProvider); await EnsureAdministratorUser(scope.ServiceProvider); + } - static async Task RegisterApplicationsAsync(IServiceProvider provider) - { - var manager = provider.GetRequiredService(); + private static async Task RegisterApplicationsAsync(IServiceProvider provider) + { + var manager = provider.GetRequiredService(); - if (await manager.FindByClientIdAsync("spa_client") is null) + if (await manager.FindByClientIdAsync("spa_client") is null) + { + await manager.CreateAsync(new OpenIddictApplicationDescriptor { - await manager.CreateAsync(new OpenIddictApplicationDescriptor - { - ClientId = "spa_client", - // ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", - ConsentType = ConsentTypes.Implicit, - DisplayName = "SPA Client Application", - PostLogoutRedirectUris = + ClientId = "spa_client", + // ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", + ConsentType = ConsentTypes.Implicit, + DisplayName = "SPA Client Application", + PostLogoutRedirectUris = { new Uri("https://localhost:5000"), new Uri("http://localhost:4200") }, - RedirectUris = + RedirectUris = { new Uri("https://localhost:5000"), new Uri("http://localhost:4200") }, - Permissions = + Permissions = { Permissions.Endpoints.Authorization, Permissions.Endpoints.Logout, @@ -81,95 +82,94 @@ await manager.CreateAsync(new OpenIddictApplicationDescriptor Permissions.Prefixes.Scope + "server_scope", Permissions.Prefixes.Scope + "api_scope" }, - Requirements = + Requirements = { Requirements.Features.ProofKeyForCodeExchange } - }); - } + }); + } - if (await manager.FindByClientIdAsync("api_service") == null) + if (await manager.FindByClientIdAsync("api_service") == null) + { + var descriptor = new OpenIddictApplicationDescriptor { - var descriptor = new OpenIddictApplicationDescriptor - { - ClientId = "api_service", - DisplayName = "API Service", - ClientSecret = "my-api-secret", - Permissions = + ClientId = "api_service", + DisplayName = "API Service", + ClientSecret = "my-api-secret", + Permissions = { Permissions.Endpoints.Introspection } - }; + }; - await manager.CreateAsync(descriptor); - } + await manager.CreateAsync(descriptor); } + } - static async Task RegisterScopesAsync(IServiceProvider provider) - { - var manager = provider.GetRequiredService(); + private static async Task RegisterScopesAsync(IServiceProvider provider) + { + var manager = provider.GetRequiredService(); - if (await manager.FindByNameAsync("server_scope") is null) + if (await manager.FindByNameAsync("server_scope") is null) + { + await manager.CreateAsync(new OpenIddictScopeDescriptor { - await manager.CreateAsync(new OpenIddictScopeDescriptor - { - Name = "server_scope", - DisplayName = "Server scope access", - Resources = + Name = "server_scope", + DisplayName = "Server scope access", + Resources = { "server" } - }); - } + }); + } - if (await manager.FindByNameAsync("api_scope") == null) + if (await manager.FindByNameAsync("api_scope") == null) + { + var descriptor = new OpenIddictScopeDescriptor { - var descriptor = new OpenIddictScopeDescriptor - { - Name = "api_scope", - DisplayName = "API Scope access", - Resources = + Name = "api_scope", + DisplayName = "API Scope access", + Resources = { "api_service" } - }; + }; - await manager.CreateAsync(descriptor); - } + await manager.CreateAsync(descriptor); } + } - static async Task EnsureAdministratorRole(IServiceProvider provider) - { - var manager = provider.GetRequiredService>(); + private static async Task EnsureAdministratorRole(IServiceProvider provider) + { + var manager = provider.GetRequiredService>(); - var role = Roles.ADMINISTRATOR_ROLE; - var roleExists = await manager.RoleExistsAsync(role); - if (!roleExists) - { - var newRole = new IdentityRole(role); - await manager.CreateAsync(newRole); - } + var role = Roles.ADMINISTRATOR_ROLE; + var roleExists = await manager.RoleExistsAsync(role); + if (!roleExists) + { + var newRole = new IdentityRole(role); + await manager.CreateAsync(newRole); } + } - static async Task EnsureAdministratorUser(IServiceProvider provider) - { - var manager = provider.GetRequiredService>(); + private static async Task EnsureAdministratorUser(IServiceProvider provider) + { + var manager = provider.GetRequiredService>(); - var user = await manager.FindByNameAsync(Constants.ADMIN_MAILADDRESS); - if (user != null) return; + var user = await manager.FindByNameAsync(Constants.ADMIN_MAILADDRESS); + if (user != null) return; - var applicationUser = new ApplicationUser - { - UserName = Constants.ADMIN_MAILADDRESS, - Email = Constants.ADMIN_MAILADDRESS - }; + var applicationUser = new ApplicationUser + { + UserName = Constants.ADMIN_MAILADDRESS, + Email = Constants.ADMIN_MAILADDRESS + }; - var userResult = await manager.CreateAsync(applicationUser, "Pass123$"); - if (!userResult.Succeeded) return; + var userResult = await manager.CreateAsync(applicationUser, "Pass123$"); + if (!userResult.Succeeded) return; - await manager.SetLockoutEnabledAsync(applicationUser, false); - await manager.AddToRoleAsync(applicationUser, Roles.ADMINISTRATOR_ROLE); - } + await manager.SetLockoutEnabledAsync(applicationUser, false); + await manager.AddToRoleAsync(applicationUser, Roles.ADMINISTRATOR_ROLE); } } } \ No newline at end of file diff --git a/src/identity/OpenIddict.UI.Identity.Api/Account/AccountApiService.cs b/src/identity/OpenIddict.UI.Identity.Api/Account/AccountApiService.cs index 878ceb4..7aee249 100644 --- a/src/identity/OpenIddict.UI.Identity.Api/Account/AccountApiService.cs +++ b/src/identity/OpenIddict.UI.Identity.Api/Account/AccountApiService.cs @@ -11,6 +11,7 @@ namespace tomware.OpenIddict.UI.Identity.Api public interface IAccountApiService { Task RegisterAsync(RegisterUserViewModel model); + Task ChangePasswordAsync(ChangePasswordViewModel model); Task> GetUsersAsync(); Task GetUserAsync(string id); Task UpdateAsync(UserViewModel model); @@ -46,6 +47,17 @@ RegisterUserViewModel model return await _manager.CreateAsync(identiyUser, model.Password); } + public async Task ChangePasswordAsync(ChangePasswordViewModel model) + { + var user = await _manager.FindByNameAsync(model.UserName); + + return await _manager.ChangePasswordAsync( + user, + model.CurrentPassword, + model.NewPassword + ); + } + public async Task> GetUsersAsync() { // TODO: Paging ??? diff --git a/src/identity/OpenIddict.UI.Identity.Api/Account/AccountController.cs b/src/identity/OpenIddict.UI.Identity.Api/Account/AccountController.cs index a5c1c5e..458ef73 100644 --- a/src/identity/OpenIddict.UI.Identity.Api/Account/AccountController.cs +++ b/src/identity/OpenIddict.UI.Identity.Api/Account/AccountController.cs @@ -24,6 +24,7 @@ IAccountApiService service [ProducesResponseType(typeof(IdentityResult), StatusCodes.Status200OK)] public async Task Register([FromBody] RegisterUserViewModel model) { + if (model == null) return BadRequest(); if (ModelState.IsValid) { var result = await _service.RegisterAsync(model); @@ -38,6 +39,25 @@ public async Task Register([FromBody] RegisterUserViewModel model return BadRequest(ModelState); } + [HttpPost("changepassword")] + [ProducesResponseType(typeof(IdentityResult), StatusCodes.Status200OK)] + public async Task ChangePassword([FromBody]ChangePasswordViewModel model) + { + if (model == null) return BadRequest(); + if (ModelState.IsValid) + { + var result = await _service.ChangePasswordAsync(model); + if (result.Succeeded) + { + return Ok(result); + } + + AddErrors(result); + } + + return BadRequest(ModelState); + } + [HttpGet("users")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task Users() diff --git a/src/identity/OpenIddict.UI.Identity.Api/Account/ChangePasswordViewModel.cs b/src/identity/OpenIddict.UI.Identity.Api/Account/ChangePasswordViewModel.cs new file mode 100644 index 0000000..8cb3b70 --- /dev/null +++ b/src/identity/OpenIddict.UI.Identity.Api/Account/ChangePasswordViewModel.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; + +namespace tomware.OpenIddict.UI.Identity.Api +{ + public class ChangePasswordViewModel + { + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + public string CurrentPassword { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + public string NewPassword { get; set; } + + [Required] + [Compare("NewPassword", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + + [Required] + public string UserName { get; set; } + } +} \ No newline at end of file diff --git a/tests/Integration/AccountApiTest.cs b/tests/Integration/AccountApiTest.cs index e2e05ba..54e8668 100644 --- a/tests/Integration/AccountApiTest.cs +++ b/tests/Integration/AccountApiTest.cs @@ -111,6 +111,38 @@ public async Task Register_UserRegistered() Assert.Equal(HttpStatusCode.OK, response.StatusCode); } + [Fact] + public async Task ChangePassword_PasswordChanged() + { + // Arrange + var email = "userToChangePassword@openiddict.com"; + await DeleteUser(email); + + await PostAsync("/api/accounts/register", new RegisterUserViewModel + { + UserName = "username", + Email = email, + Password = "Pass123$", + ConfirmPassword = "Pass123$" + }); + + var user = await FindUserByEmail(email); + Assert.NotNull(user); + + // Act + var response = await PostAsync($"/api/accounts/changepassword", new ChangePasswordViewModel + { + UserName = user.UserName, + CurrentPassword = "Pass123$", + NewPassword = "Pass1234$", + ConfirmPassword = "Pass1234$" + }); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + [Fact] public async Task GetAsync_UserReceived() { diff --git a/tests/Unit/AccountControllerTest.cs b/tests/Unit/AccountControllerTest.cs index 26d7670..a09600c 100644 --- a/tests/Unit/AccountControllerTest.cs +++ b/tests/Unit/AccountControllerTest.cs @@ -9,6 +9,35 @@ namespace tomware.OpenIddict.UI.Tests.Unit { public class AccountControllerTest { + [Fact] + public async Task Register_WithNullModel_ReturnsBadRequest() + { + // Arrange + var controller = GetController(); + + // Act + var result = await controller.Register(null); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + + [Fact] + public async Task ChangePassword_WithNullModel_ReturnsBadRequest() + { + // Arrange + var controller = GetController(); + + // Act + var result = await controller.ChangePassword(null); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + [Fact] public async Task GetUser_WithNullId_ReturnsBadRequest() {