zero user crud works (a bit clunky, but heyyy)

This commit is contained in:
2022-02-12 21:16:31 +01:00
parent c72300c168
commit c3e7ed6954
13 changed files with 218 additions and 27 deletions
+36 -6
View File
@@ -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<ActionResult<Result>> Create(ZeroUser saveModel) => CreateModel(saveModel);
[HttpPost("")]
[ZeroAuthorize(UserPermissions.Create)]
public virtual async Task<ActionResult<Result>> Create(ZeroUser saveModel)
{
Result<ZeroUser> 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<ActionResult<Result>> Delete(string id) => DeleteModel(id);
[HttpDelete("{id}")]
[ZeroAuthorize(UserPermissions.Delete)]
public virtual async Task<ActionResult<Result>> Delete(string id)
{
Result<ZeroUser> result = await Users.Delete(id);
return result.WithoutModel();
}
[HttpGet("password/random/{length?}")]
[ZeroAuthorize(UserPermissions.Read)]
public virtual ActionResult<dynamic> Password(int length = -1)
{
return new
{
Password = PasswordGenerator.Random(length)
};
}
}
+12 -10
View File
@@ -1,16 +1,15 @@
<template>
<div class="app-auth">
<span></span>
<form class="app-auth-inner" @submit.prevent="onSubmit">
<ui-form class="app-auth-inner" v-slot="form" @submit="onSubmit">
<div>
<div class="app-auth-logo">
<span class="app-nav-logo-circle"></span>
<img src="/Assets/zero.svg" class="app-auth-image show-light" v-localize:alt="'@zero.name'" />
<img src="/Assets/zero-dark.svg" class="app-auth-image show-dark" v-localize:alt="'@zero.name'" />
</div>
<!--<ui-error :catch-remaining="true" />
<ui-message type="info" v-if="rejectReason" :text="rejectReason" />-->
<ui-error :catch-remaining="true" />
<ui-property field="email" label="@login.fields.email" :vertical="true">
<input v-model="model.email" type="text" class="ui-input" maxlength="120" v-localize:placeholder="'@login.fields.email_placeholder'" />
@@ -23,10 +22,10 @@
</div>
<div class="app-auth-bottom">
<ui-button class="app-auth-confirm" type="accent big" :submit="true" label="@login.button" :state="state" />
<ui-button class="app-auth-confirm" type="accent big" :submit="true" label="@login.button" :state="form.state" />
<!--<ui-button type="blank" label="@login.button_forgot" />-->
</div>
</form>
</ui-form>
</div>
</template>
@@ -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;
}
}
@@ -0,0 +1,90 @@
<template>
<div>
<div class="ui-password-hash" :class="{ 'is-reloading': reloading }">
<input value="******************" v-if="!changing" type="text" class="ui-input" readonly="readonly" disabled="disabled" />
<input v-model="password" v-else type="text" readonly="readonly" class="ui-input" @click="$event.target.select()" />
<ui-button v-if="!disabled" :disabled="reloading" icon="fth-rotate-ccw" v-localize:title="'@changepasswordoverlay.regenerate'" type="light" @click="getRandomPassword" />
</div>
<p v-if="changing" class="ui-property-help" v-localize="'@changepasswordoverlay.regenerate_help'"></p>
</div>
</template>
<script>
import api from '../modules/users/api';
export default {
props: {
value: {
type: String,
default: null
},
disabled: {
type: Boolean,
default: false
},
placeholder: {
type: [String, Function],
default: null
},
entity: Object
},
data: () => ({
reloading: false,
password: null,
changing: false
}),
methods: {
async getRandomPassword()
{
this.reloading = true;
const result = await api.getRandomPassword(32);
this.password = result.data.password;
let passedValue = 'raw:' + this.password;
this.$emit('input', passedValue);
this.$emit('update:value', passedValue);
this.changing = true;
this.reloading = false;
}
}
}
</script>
<style lang="scss">
.ui-password-hash
{
display: grid;
grid-template-columns: 1fr auto;
grid-gap: var(--padding-xxs);
&.is-reloading .ui-icon
{
animation: rotating 1s linear infinite reverse;
}
@keyframes rotating
{
from
{
-webkit-transform: rotate(0);
transform: rotate(0)
}
to
{
-webkit-transform: rotate(1turn);
transform: rotate(1turn)
}
}
& + .ui-property-help
{
margin: 10px 0 0;
}
}
</style>
+4 -2
View File
@@ -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),
@@ -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);
@@ -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
@@ -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",
@@ -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",
+21
View File
@@ -0,0 +1,21 @@
namespace zero.Identity;
public class PasswordGenerator
{
const string CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-@#.:!?*";
private static Random random = new();
/// <summary>
/// Create a new password
/// </summary>
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());
}
}
+39 -5
View File
@@ -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<string>()
{
"userRoles.c9ozj4vboing",
"userRoles.nc5ezcu660fo"
};
}
model.Username = model.Email?.ToUpper();
BackofficeUserValidator validator = new(!isUpdate);
ValidationResult validation = await validator.ValidateAsync(model);
if (!validation.IsValid)
{
return Result<ZeroUser>.Fail(validation);
}
Result<ZeroUser> result = isUpdate ? await Operations.Update(model) : await Operations.Create(model);
@@ -6,9 +6,9 @@ public class BackofficeUserValidator : ZeroValidator<ZeroUser>
{
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();
+3 -1
View File
@@ -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<ZeroUser, ZeroUserRole>();
services.Replace<IUserClaimsPrincipalFactory<ZeroUser>, ZeroBackofficeClaimsPrincipalFactory<ZeroUser, ZeroUserRole>>();
services.Replace<IUserStore<ZeroUser>, RavenCoreUserStore<ZeroUser, ZeroUserRole>>(ServiceLifetime.Scoped);
services.AddScoped<IValidator<ZeroUser>, BackofficeUserValidator>();
services.Replace<IRoleStore<ZeroUserRole>, RavenCoreRoleStore<ZeroUserRole>>(ServiceLifetime.Scoped);
services.AddSingleton<IAuthenticationSchemeProvider, ZeroAuthenticationSchemeProvider>();