From c3e7ed6954e1d4e84003cb787d701cdc292ba53c Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Sat, 12 Feb 2022 21:16:31 +0100 Subject: [PATCH] zero user crud works (a bit clunky, but heyyy) --- ...s_Listing.cs => zero_Api_Users_Listing.cs} | 0 zero.Api/Endpoints/Users/UsersController.cs | 42 +++++++-- zero.Backoffice.UI/app/account/login.vue | 22 ++--- .../app/components/ui-password-hash.vue | 90 +++++++++++++++++++ zero.Backoffice.UI/app/modules/users/api.ts | 6 +- .../app/modules/users/schemas/editor.ts | 3 +- .../app/styles/Core/_forms.scss | 5 ++ .../Resources/Localization/zero.de-de.json | 2 + .../Resources/Localization/zero.en-us.json | 2 + zero.Core/Identity/PasswordGenerator.cs | 21 +++++ zero.Core/Identity/Services/UserService.cs | 44 +++++++-- .../Validation/BackofficeUserValidator.cs | 4 +- zero.Core/Identity/ZeroIdentityModule.cs | 4 +- 13 files changed, 218 insertions(+), 27 deletions(-) rename zero.Api/Endpoints/Users/Indexes/{zero_Api_Countries_Listing.cs => zero_Api_Users_Listing.cs} (100%) create mode 100644 zero.Backoffice.UI/app/components/ui-password-hash.vue create mode 100644 zero.Core/Identity/PasswordGenerator.cs diff --git a/zero.Api/Endpoints/Users/Indexes/zero_Api_Countries_Listing.cs b/zero.Api/Endpoints/Users/Indexes/zero_Api_Users_Listing.cs similarity index 100% rename from zero.Api/Endpoints/Users/Indexes/zero_Api_Countries_Listing.cs rename to zero.Api/Endpoints/Users/Indexes/zero_Api_Users_Listing.cs diff --git a/zero.Api/Endpoints/Users/UsersController.cs b/zero.Api/Endpoints/Users/UsersController.cs index 0282ca5a..7d27d3e3 100644 --- a/zero.Api/Endpoints/Users/UsersController.cs +++ b/zero.Api/Endpoints/Users/UsersController.cs @@ -31,6 +31,9 @@ public class UsersController : ZeroApiController return NotFound(); } + model.PasswordHash = null; + model.SecurityStamp = null; + //HttpContext.Items[ApiConstants.ChangeToken] = Store.GetChangeToken(model); return model; @@ -48,9 +51,21 @@ public class UsersController : ZeroApiController } - //[HttpPost("")] - //[ZeroAuthorize(UserPermissions.Create)] - //public virtual Task> Create(ZeroUser saveModel) => CreateModel(saveModel); + [HttpPost("")] + [ZeroAuthorize(UserPermissions.Create)] + public virtual async Task> Create(ZeroUser saveModel) + { + Result result = await Users.Save(saveModel); + + bool minimalResponse = Hints.ResponsePreference == ApiResponsePreference.Minimal; + + if (result.IsSuccess) + { + return Created("/", minimalResponse ? null : saveModel); + } + + return result.WithoutModel(); + } [HttpPut("{id}")] @@ -73,7 +88,22 @@ public class UsersController : ZeroApiController } - //[HttpDelete("{id}")] - //[ZeroAuthorize(UserPermissions.Delete)] - //public virtual Task> Delete(string id) => DeleteModel(id); + [HttpDelete("{id}")] + [ZeroAuthorize(UserPermissions.Delete)] + public virtual async Task> Delete(string id) + { + Result result = await Users.Delete(id); + return result.WithoutModel(); + } + + + [HttpGet("password/random/{length?}")] + [ZeroAuthorize(UserPermissions.Read)] + public virtual ActionResult Password(int length = -1) + { + return new + { + Password = PasswordGenerator.Random(length) + }; + } } \ No newline at end of file diff --git a/zero.Backoffice.UI/app/account/login.vue b/zero.Backoffice.UI/app/account/login.vue index 4d959fea..6f2712ec 100644 --- a/zero.Backoffice.UI/app/account/login.vue +++ b/zero.Backoffice.UI/app/account/login.vue @@ -1,16 +1,15 @@  @@ -42,7 +41,6 @@ data: () => ({ rejectReason: null, - state: 'default', model: { email: '', password: '', @@ -61,7 +59,7 @@ const accountStore = useAccountStore(); this.rejectReason = null; - this.state = 'loading'; + form.setState('loading'); const response = await api.login(this.model); @@ -72,12 +70,16 @@ if (userResponse.success) { this.zero.events.emit('zero.authenticate', userResponse.data); - this.state = 'success'; + form.handle(response); return; } } + else + { + form.handle(response); + } - this.state = 'error'; + //this.state = 'error'; return; } } diff --git a/zero.Backoffice.UI/app/components/ui-password-hash.vue b/zero.Backoffice.UI/app/components/ui-password-hash.vue new file mode 100644 index 00000000..7212826f --- /dev/null +++ b/zero.Backoffice.UI/app/components/ui-password-hash.vue @@ -0,0 +1,90 @@ + + + + + + \ No newline at end of file diff --git a/zero.Backoffice.UI/app/modules/users/api.ts b/zero.Backoffice.UI/app/modules/users/api.ts index 34fc5f6a..ed152213 100644 --- a/zero.Backoffice.UI/app/modules/users/api.ts +++ b/zero.Backoffice.UI/app/modules/users/api.ts @@ -8,11 +8,13 @@ export default { getByQuery: (query: ApiRequestQuery, config?: ApiRequestConfig) => get('users', { ...config, params: { ...query } }), - //create: (model: any, config?: ApiRequestConfig) => post('countries', model, config), + create: (model: any, config?: ApiRequestConfig) => post('users', model, config), update: (model: any, config?: ApiRequestConfig) => put('users/' + model.id, model, config), - //delete: (id: string, config?: ApiRequestConfig) => del('countries/' + id, null, config), + delete: (id: string, config?: ApiRequestConfig) => del('users/' + id, null, config), + + getRandomPassword: (length?: number, config?: ApiRequestConfig) => get("users/password/random" + (length ? '/' + length : ''), { ...config }), //updatePassword: async model => await post(base + 'updatePassword', model), diff --git a/zero.Backoffice.UI/app/modules/users/schemas/editor.ts b/zero.Backoffice.UI/app/modules/users/schemas/editor.ts index e98114ef..fa7705f2 100644 --- a/zero.Backoffice.UI/app/modules/users/schemas/editor.ts +++ b/zero.Backoffice.UI/app/modules/users/schemas/editor.ts @@ -1,5 +1,6 @@  import { ZeroEditor } from "../../../editor/editor"; +import UiPasswordHash from '../../../components/ui-password-hash.vue'; //import Permissions from 'zero/components/permissions.vue'; const editor = new ZeroEditor('users'); @@ -18,7 +19,7 @@ permissions.hidden = x => !x.id; general.field('name', { label: '@ui.name' }).text({ maxLength: 80 }); general.field('email').text({ maxLength: 120 }); -general.field('passwordHash', { hidden: x => !x.id }).passwordHash(); +general.field('passwordHash', { hidden: x => !x.id }).component(UiPasswordHash); general.field('languageId').culturePicker(); general.field('avatarId', { optional: true }).image(); //permissions.field('claims', { hideLabel: true }).custom(Permissions); diff --git a/zero.Backoffice.UI/app/styles/Core/_forms.scss b/zero.Backoffice.UI/app/styles/Core/_forms.scss index 73e569cb..605a946e 100644 --- a/zero.Backoffice.UI/app/styles/Core/_forms.scss +++ b/zero.Backoffice.UI/app/styles/Core/_forms.scss @@ -37,6 +37,11 @@ textarea, .ui-native-select, .ui-rte-input, .flatpickr-current-month box-shadow: var(--color-input-focus-shadow); outline: none; } + + &:read-only, &:-moz-read-only, &[readonly]:focus + { + outline: none !important; + } } select:focus diff --git a/zero.Backoffice/Resources/Localization/zero.de-de.json b/zero.Backoffice/Resources/Localization/zero.de-de.json index a7152c33..418b1604 100644 --- a/zero.Backoffice/Resources/Localization/zero.de-de.json +++ b/zero.Backoffice/Resources/Localization/zero.de-de.json @@ -302,6 +302,8 @@ "changepasswordoverlay": { "title": "Passwort ändern", "confirm": "Passwort ändern", + "regenerate": "Neu generieren", + "regenerate_help": "Kopiere das neue Passwort, bevor du den Benutzer speicherst", "fields": { "currentpassword": "Aktuelles Passwort", "newpassword": "Neues Passwort", diff --git a/zero.Backoffice/Resources/Localization/zero.en-us.json b/zero.Backoffice/Resources/Localization/zero.en-us.json index 09d40aee..a415923f 100644 --- a/zero.Backoffice/Resources/Localization/zero.en-us.json +++ b/zero.Backoffice/Resources/Localization/zero.en-us.json @@ -314,6 +314,8 @@ "changepasswordoverlay": { "title": "Change password", "confirm": "Change password", + "regenerate": "Regenerate", + "regenerate_help": "Copy the new password before updating the user", "fields": { "currentPassword": "Current password", "newPassword": "New password", diff --git a/zero.Core/Identity/PasswordGenerator.cs b/zero.Core/Identity/PasswordGenerator.cs new file mode 100644 index 00000000..be20c9dd --- /dev/null +++ b/zero.Core/Identity/PasswordGenerator.cs @@ -0,0 +1,21 @@ +namespace zero.Identity; + +public class PasswordGenerator +{ + const string CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-@#.:!?*"; + + private static Random random = new(); + + /// + /// Create a new password + /// + public static string Random(int length = -1) + { + if (length < 1) + { + length = 12; + } + + return new string(Enumerable.Repeat(CHARS, length).Select(s => s[random.Next(s.Length)]).ToArray()); + } +} diff --git a/zero.Core/Identity/Services/UserService.cs b/zero.Core/Identity/Services/UserService.cs index 9656c4be..fa557b09 100644 --- a/zero.Core/Identity/Services/UserService.cs +++ b/zero.Core/Identity/Services/UserService.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Identity; +using FluentValidation.Results; +using Microsoft.AspNetCore.Identity; using Raven.Client.Documents; using Raven.Client.Documents.Linq; @@ -63,13 +64,46 @@ public class UserService : IUserService bool updateSecurityStamp = false; bool isUpdate = false; - if (!model.Id.IsNullOrEmpty()) + ZeroUser input = model; + + if (model.Id.HasValue()) { - // TODO throws "Attempted to associate a different object with id ..." - //ZeroUser origin = await GetUserById(model.Id); - isUpdate = true; // origin != null; + model = await GetUserById(model.Id); + + if (model != null) + { + if (input.PasswordHash.StartsWith("raw:")) + { + string newPassword = input.PasswordHash.Substring(4); + model.PasswordHash = UserManager.PasswordHasher.HashPassword(model, newPassword); + } + + ObjectCopycat.CopyProperties(input, model, nameof(ZeroUser.PasswordHash), nameof(ZeroUser.SecurityStamp)); + } + + isUpdate = model != null; updateSecurityStamp = true; // origin != null && model.PasswordHash != origin.PasswordHash; } + else + { + // this is bullshit ;-) + model.AppId = "app.hofbauer"; + model.CurrentAppId = "app.hofbauer"; + model.RoleIds = new List() + { + "userRoles.c9ozj4vboing", + "userRoles.nc5ezcu660fo" + }; + } + + model.Username = model.Email?.ToUpper(); + BackofficeUserValidator validator = new(!isUpdate); + ValidationResult validation = await validator.ValidateAsync(model); + + if (!validation.IsValid) + { + return Result.Fail(validation); + } Result result = isUpdate ? await Operations.Update(model) : await Operations.Create(model); diff --git a/zero.Core/Identity/Validation/BackofficeUserValidator.cs b/zero.Core/Identity/Validation/BackofficeUserValidator.cs index 424bd32a..c022b50e 100644 --- a/zero.Core/Identity/Validation/BackofficeUserValidator.cs +++ b/zero.Core/Identity/Validation/BackofficeUserValidator.cs @@ -6,9 +6,9 @@ public class BackofficeUserValidator : ZeroValidator { public BackofficeUserValidator(bool isCreate = false) { - RuleFor(x => x.Name).Length(2, 80); + RuleFor(x => x.Name).NotEmpty().Length(2, 80); RuleFor(x => x.Email).NotEmpty().Email().MaximumLength(120); - RuleFor(x => x.PasswordHash).NotEmpty(); + //RuleFor(x => x.PasswordHash).NotEmpty(); RuleFor(x => x.LanguageId).NotEmpty(); RuleFor(x => x.RoleIds).NotEmpty(); diff --git a/zero.Core/Identity/ZeroIdentityModule.cs b/zero.Core/Identity/ZeroIdentityModule.cs index 34ff3360..e3eef001 100644 --- a/zero.Core/Identity/ZeroIdentityModule.cs +++ b/zero.Core/Identity/ZeroIdentityModule.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Authentication; +using FluentValidation; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; @@ -17,6 +18,7 @@ internal class ZeroIdentityModule : ZeroModule services.AddZeroIdentity(); services.Replace, ZeroBackofficeClaimsPrincipalFactory>(); services.Replace, RavenCoreUserStore>(ServiceLifetime.Scoped); + services.AddScoped, BackofficeUserValidator>(); services.Replace, RavenCoreRoleStore>(ServiceLifetime.Scoped); services.AddSingleton();