zero user crud works (a bit clunky, but heyyy)
This commit is contained in:
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user