users API start

This commit is contained in:
2021-12-26 01:42:21 +01:00
parent 0bd2000026
commit ffdb0c190d
42 changed files with 465 additions and 494 deletions
@@ -0,0 +1,17 @@
using Raven.Client.Documents.Indexes;
namespace zero.Api.Endpoints.Users;
public class zero_Api_Users_Listing : ZeroIndex<ZeroUser>
{
protected override void Create()
{
Map = items => items.Select(item => new
{
Name = item.Name,
CreatedDate = item.CreatedDate
});
Index(x => x.Name, FieldIndexing.Search);
}
}
@@ -0,0 +1,12 @@
namespace zero.Api.Endpoints.Users;
public class UserBasic : ZeroIdEntity
{
public string Name { get; set; }
public bool IsActive { get; set; }
public string Email { get; set; }
public string AvatarId { get; set; }
}
@@ -0,0 +1,19 @@
namespace zero.Api.Endpoints.Users;
public class UserMapperProfile : ZeroMapperProfile
{
public override void Configure(IZeroMapper mapper)
{
mapper.Define<ZeroUser, UserBasic>((source, ctx) => new(), Map);
}
protected virtual void Map(ZeroUser source, UserBasic target, IZeroMapperContext ctx)
{
target.Id = source.Id;
target.Name = source.Name;
target.Email = source.Email;
target.IsActive = source.IsActive;
target.AvatarId = source.AvatarId;
}
}
+18
View File
@@ -0,0 +1,18 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace zero.Api.Endpoints.Users;
public class UserModule : ZeroModule
{
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IPermissionProvider, UserPermissions>();
services.AddSingleton<IMapperProfile, UserMapperProfile>();
services.Configure<RavenOptions>(opts =>
{
opts.Indexes.Add<zero_Api_Users_Listing>();
});
}
}
@@ -0,0 +1,31 @@
namespace zero.Api.Endpoints.Users;
public class UserPermissions : PermissionProvider
{
public const string Group = "zero.user";
public const string Create = "zero.user.create";
public const string Read = "zero.user.read";
public const string Update = "zero.user.update";
public const string Delete = "zero.user.delete";
public override Task Configure(IPermissionContext context)
{
PermissionGroup group = new(Group, "@user.list");
group.Permissions.Add(new Permission("zero.user.defaults", "Default permissions")
{
Children = new()
{
new(Create, "@permission.states.create"),
new(Read, "@permission.states.read"),
new(Update, "@permission.states.update"),
new(Delete, "@permission.states.delete")
}
});
context.AddGroup(group);
return Task.CompletedTask;
}
}
@@ -1,15 +0,0 @@
//namespace zero.Web.Models
//{
// public class UserListModel : ListModel
// {
// public string Name { get; set; }
// public bool IsActive { get; set; }
// public string Email { get; set; }
// public string Roles { get; set; }
// public string AvatarId { get; set; }
// }
//}
@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Mvc;
namespace zero.Api.Endpoints.Users;
public class UsersController : ZeroApiController
{
readonly IUserService Users;
public UsersController(IUserService users)
{
Users = users;
}
[HttpGet("empty")]
[ZeroAuthorize(UserPermissions.Create)]
public virtual async Task<ActionResult<ZeroUser>> Empty(string flavor = null)
{
return new ZeroUser();
}
[HttpGet("{id}")]
[ZeroAuthorize(UserPermissions.Read)]
public virtual async Task<ActionResult<ZeroUser>> Get(string id)
{
ZeroUser model = await Users.GetUserById(id);
if (model == null)
{
return NotFound();
}
//HttpContext.Items[ApiConstants.ChangeToken] = Store.GetChangeToken(model);
return model;
}
[HttpGet("")]
[ZeroAuthorize(UserPermissions.Read)]
public virtual async Task<ActionResult<Paged>> Get([FromQuery] ListQuery<ZeroUser> query)
{
query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate);
query.SearchSelector ??= x => x.Name;
Paged<ZeroUser> result = await Users.GetAll(query.Page, query.PageSize);
return Mapper.Map<ZeroUser, UserBasic>(result);
}
//[HttpPost("")]
//[ZeroAuthorize(UserPermissions.Create)]
//public virtual Task<ActionResult<Result>> Create(ZeroUser saveModel) => CreateModel(saveModel);
//[HttpPut("{id}")]
//[ZeroAuthorize(UserPermissions.Update)]
//public virtual Task<ActionResult<Result>> Update(string id, ZeroUser updateModel, [FromQuery] string changeToken = null) => UpdateModel(id, updateModel, changeToken);
//[HttpDelete("{id}")]
//[ZeroAuthorize(UserPermissions.Delete)]
//public virtual Task<ActionResult<Result>> Delete(string id) => DeleteModel(id);
}
+1
View File
@@ -38,6 +38,7 @@ public class ZeroApiPlugin : ZeroPlugin
modules.Add<Endpoints.Media.MediaModule>();
modules.Add<Endpoints.Spaces.SpaceModule>();
modules.Add<Endpoints.Integrations.IntegrationModule>();
modules.Add<Endpoints.Users.UserModule>();
modules.ConfigureServices(services, configuration);
@@ -1,101 +0,0 @@
import Vue from 'vue';
import AppConfirm from 'zero/components/overlays/confirm.vue'; // TODO importing vue files in js/ts files causes a Rollup production build error
import AppMessage from 'zero/components/overlays/message.vue';
import Strings from 'zero/helpers/strings.js';
import { find as _find, extend as _extend } from 'underscore';
export default new Vue({
data: () => ({
dropdownInstance: null,
instances: []
}),
methods: {
// opens an overlay
open(options)
{
const defaultWidth = options.display === 'editor' ? 560 : 460;
options = _extend({
id: Strings.guid(),
display: 'dialog',
width: defaultWidth,
hide: this.close,
autoclose: true,
softdismiss: options.display !== 'editor',
closeLabel: '@ui.close',
confirmLabel: '@ui.confirm',
confirmType: 'default',
alias: options.alias
}, options);
if (typeof options.theme === 'undefined')
{
options.theme = 'default';
}
this.instances.push(options);
return new Promise((resolve, reject) =>
{
options.close = () =>
{
this.close(options);
reject(options);
// TODO should we move to resolve here, so we don't trigger errors in case the implementation does not catch them?
// this will at least need some tests if the .then callback does not catch null values
};
options.hide = options.close;
options.confirm = data =>
{
if (options.autoclose)
{
this.close(options);
}
resolve(data, options);
};
});
},
// closes an overlay
close(instance)
{
if (this.instances.length < 1)
{
return;
}
if (!instance)
{
this.instances.pop();
return;
}
if (typeof instance === 'string')
{
instance = _find(this.instances, item => item.id === instance);
}
if (instance)
{
const index = this.instances.indexOf(instance);
this.instances.splice(index, 1);
}
},
// closes all overlays
closeAll()
{
this.instances.forEach(instance =>
{
});
}
}
});
@@ -5,7 +5,9 @@
<ui-dropdown v-else ref="dropdown" align="right">
<template v-slot:button>
<ui-button :label="label" :type="type" :disabled="disabled" />
<slot>
<ui-button :label="label" :type="type" :disabled="disabled" />
</slot>
</template>
<template v-if="flavors.length > 0">
+3 -1
View File
@@ -19,7 +19,8 @@ import
pagePlugin,
mailTemplatePlugin,
translationPlugin,
integrationPlugin
integrationPlugin,
userPlugin
} from '../modules';
import editorPlugin from '../editor/plugin';
import { ZeroSchema } from 'zero/schemas';
@@ -111,6 +112,7 @@ export class ZeroRuntime implements Zero
mailTemplatePlugin.install(pluginOptions);
translationPlugin.install(pluginOptions);
integrationPlugin.install(pluginOptions);
userPlugin.install(pluginOptions);
}
+2 -1
View File
@@ -8,4 +8,5 @@ export * from './spaces';
export * from './pages';
export * from './mails';
export * from './translations';
export * from './integrations';
export * from './integrations';
export * from './users';
@@ -17,8 +17,11 @@
<span class="ui-tag" v-for="tag in model.tags">{{tag}}</span>
</div>-->
<div class="integrations-item-bottom">
<ui-button class="integrations-item-button" type="action small" v-if="!model.isConfigured" v-localize="'Setup'" @click="open" />
<ui-button class="integrations-item-button" type="action small" v-else v-localize="'Edit'" @click="open" />
<!--<ui-add-button :route="createRoute" alias="countries" v-if="!model.isConfigured">
<ui-button class="integrations-item-button" type="action small" v-localize="'Setup'" />
</ui-add-button>-->
<ui-button v-if="!model.isConfigured" class="integrations-item-button" type="action small" v-localize="'Setup'" @click="open" />
<ui-button v-else class="integrations-item-button" type="action small" v-localize="'Edit'" @click="open" />
<span class="integrations-item-active" v-if="model.isConfigured && model.isActivated">
<ui-icon symbol="fth-check-circle" :size="16" /> <span>Active</span>
</span>
@@ -0,0 +1,15 @@
import { ZeroEditor } from "../../editor/editor";
import { formatDate } from '../../utils/dates';
const editor = new ZeroEditor('demo');
editor.field('name', { label: 'Text' }).text({ maxLength: 120 });
editor.field('isActive', { label: 'Toggle', horizontal: true }).toggle();
editor.field('number', { label: 'Number' }).number();
editor.field('output', { label: 'Number' }).output();
editor.field('rte', { label: 'Number' }).rte();
editor.field('textarea', { label: 'Number' }).textarea();
editor.field('state', { label: 'Number' }).state({ items: [{ label: 'Green', value: 'green' }, { label: 'Red', value: 'red' }] });
export default editor;
@@ -0,0 +1,28 @@
<template>
<div>
<ui-header-bar title="Demo" />
<ui-editor config="demo" v-model="model" :meta="meta" :disabled="disabled" />
</div>
</template>
<script>
//import api from './api';
export default {
data: () => ({
meta: {},
model: {
name: 'Tobi',
isActive: true,
number: 17,
output: 'This is an output',
rte: '',
textarea: 'A textarea\nfor you',
state: 'red'
},
route: 'demo',
disabled: false
})
}
</script>
@@ -6,5 +6,8 @@ export default {
install(app: ZeroPluginOptions)
{
app.route({ name: 'settings', path: '/settings', component: () => import('./settings.vue') });
app.route({ name: 'demo', path: '/settings/demo', component: () => import('./demo.vue') });
app.schema('demo', () => import('./demo'));
}
} as ZeroPlugin;
@@ -44,6 +44,18 @@
{
this.groups = useUiStore().settingGroups;
this.appCount = useAppStore().applications.length;
if (!this.groups[1].items.find(x => x.alias === 'demo'))
{
this.groups[1].items.push({
alias: "demo",
description: "Demo all editor components",
icon: "fth-box",
isPlugin: true,
name: "Components",
url: "/settings/demo"
});
}
}
}
</script>
+11 -5
View File
@@ -11,13 +11,19 @@ export default {
getEmpty: (alias: string, config?: ApiRequestConfig) => get("spaces/empty", { ...config, params: { alias } }),
//getById: (id: string, changeVector?: string, config?: ApiRequestConfig) => get('countries/' + id, { ...config, params: { changeVector } }),
//getByAlias: async alias => await get(base + 'getByAlias', { params: { alias } }),
//getByQuery: (query: ApiRequestQuery, config?: ApiRequestConfig) => get('countries', { ...config, params: { ...query } }),
//getAll: async () => await get(base + 'getAll'),
//create: (model: any, config?: ApiRequestConfig) => post('countries', model, config),
//getList: async (alias, query) =>
//{
// query.alias = alias;
// return await get(base + 'getList', { params: query })
//},
//update: (model: any, config?: ApiRequestConfig) => put('countries/' + model.id, model, config),
//getContent: async (alias, contentId) => await get(base + 'getContent', { params: { alias, contentId } }),
//delete: (id: string, config?: ApiRequestConfig) => del('countries/' + id, null, config),
//save: async model => await post(base + 'save', model),
//delete: async (alias, id) => await del(base + 'delete', { params: { alias, id } })
};
@@ -0,0 +1,24 @@
import { get, post, put, del, ApiRequestConfig, ApiRequestQuery } from '../../services/request';
export default {
getEmpty: (flavor?: string, config?: ApiRequestConfig) => get("users/empty", { ...config, params: { flavor } }),
getById: (id: string, changeVector?: string, config?: ApiRequestConfig) => get('users/' + id, { ...config, params: { changeVector } }),
getByQuery: (query: ApiRequestQuery, config?: ApiRequestConfig) => get('users', { ...config, params: { ...query } }),
//create: (model: any, config?: ApiRequestConfig) => post('countries', model, config),
//update: (model: any, config?: ApiRequestConfig) => put('countries/' + model.id, model, config),
//delete: (id: string, config?: ApiRequestConfig) => del('countries/' + id, null, config),
//updatePassword: async model => await post(base + 'updatePassword', model),
//hashPassword: async model => await post(base + 'hashPassword', model),
//disable: async model => await post(base + 'disable', model),
//enable: async model => await post(base + 'enable', model)
};
@@ -0,0 +1,7 @@
import api from './api';
import plugin from './plugin';
export {
api as userApi,
plugin as userPlugin
};
@@ -0,0 +1,17 @@
import { ZeroPlugin, ZeroPluginOptions } from '../../core';
import { defineAsyncComponent } from 'vue';
export default {
name: "zero.users",
install(app: ZeroPluginOptions)
{
//app.vue.component('ui-countrypicker', defineAsyncComponent(() => import('./ui-countrypicker.vue')));
app.route({ name: 'users', path: '/settings/users', component: () => import('./users.vue') });
app.route({ name: 'users-edit', path: '/settings/users/edit/:id?', component: () => import('./user.vue'), props: true });
app.schema('users', () => import('./schemas/list'));
app.schema('users:edit', () => import('./schemas/editor'));
}
} as ZeroPlugin;
@@ -0,0 +1,25 @@
import { ZeroEditor } from "../../../editor/editor";
//import Permissions from 'zero/components/permissions.vue';
const editor = new ZeroEditor('users');
editor.resourcePrefix = '@user.fields.';
//const permissionsCount = x => (x.claims || []).filter(claim =>
//{
// const value = claim.value.split(':')[1];
// return value !== 'none' && value !== 'false' && !!value;
//}).length;
const general = editor.tab('general', '@ui.tab_general');
const permissions = editor.tab('permissions', '@user.tab_permissions'); //, permissionsCount, 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('languageId').culturePicker();
general.field('avatarId', { optional: true }).image();
//permissions.field('claims', { hideLabel: true }).custom(Permissions);
export default editor;
@@ -1,6 +1,6 @@

import List from 'zero/core/list.ts';
import UsersApi from 'zero/api/users.js';
import List from '../../../schemas/list/list';
import api from '../api';
const list = new List('users');
const prefix = '@user.fields.';
@@ -9,13 +9,13 @@ list.templateLabel = x => prefix + x;
list.link = x =>
{
return {
name: zero.alias.settings.users + '-edit',
name: 'users-edit',
params: { id: x.id },
query: { scope: 'shared' }
};
};
list.onFetch(filter => UsersApi.getAll(filter));
list.onFetch(filter => api.getByQuery(filter));
list.column('avatarId', { width: 70, canSort: false, hideLabel: true }).image();
list.column('name').name();
@@ -0,0 +1,49 @@
<template>
<ui-form ref="form" class="user" v-slot="form" @submit="onSubmit" @load="onLoad" :route="route">
<ui-form-header v-model:value="model" prefix="@user.users" title="@user.name" :disabled="disabled" :is-create="!id" :state="form.state" :can-delete="meta.canDelete" @delete="onDelete" />
<ui-editor config="users:edit" v-model="model" :meta="meta" :disabled="disabled">
<template v-slot:below>
<ui-editor-infos v-model="model" :disabled="disabled" />
</template>
</ui-editor>
</ui-form>
</template>
<script>
import api from './api';
export default {
props: ['id'],
data: () => ({
meta: {},
model: { name: null },
route: 'users-edit',
disabled: false
}),
methods: {
async onLoad(form)
{
const response = await form.load(() => this.id ? api.getById(this.id) : api.getEmpty(this.$route.query['zero.flavor']));
this.model = response;
},
async onSubmit(form)
{
const response = this.id ? await api.update(this.model) : await api.create(this.model);
await form.handle(response);
},
onDelete(item, opts)
{
opts.hide();
this.$refs.form.onDelete(api.delete.bind(this, this.id));
}
}
}
</script>
@@ -0,0 +1,39 @@
<template>
<div class="users">
<ui-header-bar title="@user.users" :count="count" :back-button="true">
<ui-table-filter :attach="$refs.table" />
<ui-add-button :route="createRoute" alias="countries" />
</ui-header-bar>
<div class="ui-blank-box">
<ui-table ref="table" config="users" @count="count = $event" />
</div>
</div>
</template>
<script lang="ts">
export default {
data: () => ({
count: 0,
createRoute: 'users-edit'
})
}
</script>
<style lang="scss">
.users .ui-table-field-image
{
border-radius: 50px;
}
.users .ui-table-cell[table-field="avatarId"]
{
padding: 12px;
padding-left: 20px;
}
.users .ui-table-cell[table-field="name"]
{
border-left-color: transparent;
}
</style>
@@ -200,7 +200,7 @@ class ListColumn
{
this._type = 'image';
this._asHtml = true;
this._func = (value, opts) => value ? `<img src="/zero/api/backoffice/ui/mediapreview/${(value)}" class="ui-table-field-image">` : '';
this._func = (value, opts) => value ? `<img src="/zero/api/system/media/${(value)}/thumb.tmp" class="ui-table-field-image">` : '';
return this;
}
@@ -1,7 +0,0 @@
import { collection, get } from '../helpers/request.ts';
export default {
...collection('applications/'),
getAllFeatures: async () => await get('applications/getAllFeatures')
};
@@ -1,13 +0,0 @@
import { get, post, del } from 'zero/helpers/request.ts';
const base = 'integrations/';
export default {
getEmpty: async alias => await get(base + 'getEmpty', { params: { alias } }),
getTypes: async () => await get(base + 'getTypes'),
getByAlias: async (alias, config) => await get(base + 'getByAlias', { ...config, params: { alias } }),
getByQuery: async (query, config) => await get(base + 'getByQuery', { ...config, params: { query } }),
save: async (model, config) => await post(base + 'save', model, { ...config }),
saveActiveState: async (model, config) => await post(base + 'saveActiveState', model, { ...config }),
delete: async alias => await del(base + 'delete', { params: { alias } })
};
-21
View File
@@ -1,21 +0,0 @@
import { get, post, del } from '../helpers/request.ts';
const base = 'spaces/';
export default {
getByAlias: async alias => await get(base + 'getByAlias', { params: { alias } }),
getAll: async () => await get(base + 'getAll'),
getList: async (alias, query) =>
{
query.alias = alias;
return await get(base + 'getList', { params: query })
},
getContent: async (alias, contentId) => await get(base + 'getContent', { params: { alias, contentId } }),
save: async model => await post(base + 'save', model),
delete: async(alias, id) => await del(base + 'delete', { params: { alias, id } })
};
-15
View File
@@ -1,15 +0,0 @@
import { collection, post } from '../helpers/request.ts';
const base = 'users/';
export default {
...collection(base),
updatePassword: async model => await post(base + 'updatePassword', model),
hashPassword: async model => await post(base + 'hashPassword', model),
disable: async model => await post(base + 'disable', model),
enable: async model => await post(base + 'enable', model)
};
@@ -1,155 +0,0 @@
<template>
<div class="page page-error">
<ui-icon class="page-error-icon" symbol="fth-cloud-snow" :size="82" />
<p class="page-error-text">
<strong class="page-error-headline">Not found</strong><br>
The requested resource could not be found
<br>
<code>{{path}}</code>
</p>
<ui-button class="page-error-button" type="light onbg" :label="detailsText" @click="details = !details" />
<template v-if="details">
<br><br>
<div class="page-error-routes">
<span>#</span>
<span>Name</span>
<span>Path</span>
<template v-for="(route, index) in routes">
<span>{{index + 1}}.</span>
<b>{{route.name}}</b>
<span>{{route.path}}</span>
</template>
</div>
</template>
</div>
</template>
<script>
export default {
data: () => ({
path: null,
routes: [],
details: false
}),
watch: {
'$route': function (val)
{
this.rebuild();
}
},
computed: {
detailsText()
{
return this.details ? 'Hide' : 'Defined routes';
}
},
mounted()
{
this.rebuild();
},
methods: {
rebuild()
{
this.path = this.$router.options.base + this.$route.path.substring(1);
this.routes = [];
this.$router.getRoutes().forEach(route =>
{
this.routes.push({
path: route.path,
name: route.name
});
if (route.children)
{
route.children.forEach(child =>
{
this.routes.push({
path: route.path + '/' + child.path,
name: child.name
});
});
}
});
}
}
}
</script>
<style lang="scss">
.page-error
{
width: 100%;
min-height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: var(--color-text);
text-align: center;
padding: var(--padding);
overflow-y: auto;
}
.page-error-icon
{
color: var(--color-text);
margin-bottom: 20px;
}
.page-error-text
{
font-size: var(--font-size);
color: var(--color-text-dim);
line-height: 1.4em;
}
.page-error-headline
{
display: inline-block;
margin-bottom: 10px;
font-size: var(--font-size-l);
color: var(--color-text);
}
.page-error-button
{
margin-top: 10px;
}
.page-error-routes
{
display: grid;
grid-template-columns: auto auto 1fr;
width: 100%;
max-width: 100%;
text-align: left;
border-top: 1px solid var(--color-line-onbg);
border-left: 1px solid var(--color-line-onbg);
margin-top: 30px;
span, b
{
border: 1px solid var(--color-line-onbg);
border-left: none;
border-top: none;
padding: 8px 10px 6px;
font-family: Consolas;
font-size: 12px;
&:nth-child(3n+1)
{
color: var(--color-text-dim);
}
}
}
</style>
@@ -1,22 +0,0 @@
import application from './application.js';
import country from './country.js';
import language from './language.js';
import media from './media.js';
import translation from './translation.js';
import user from './user.js';
import userRole from './userRole.js';
import mailTemplate from './mailTemplate.js';
import pageFolder from './page-folder.js';
export default {
application,
country,
language,
media,
translation,
user,
userRole,
mailTemplate,
pageFolder
};
@@ -1,27 +0,0 @@
import Editor from 'zero/core/editor.ts';
import Permissions from 'zero/components/permissions.vue';
const editor = new Editor('user', '@user.fields.');
editor.options.coreDatabase = true;
const permissionsCount = x => (x.claims || []).filter(claim =>
{
const value = claim.value.split(':')[1];
return value !== 'none' && value !== 'false' && !!value;
}).length;
const general = editor.tab('general', '@ui.tab_general');
const permissions = editor.tab('permissions', '@user.tab_permissions', permissionsCount, x => !x.id);
general.field('name', { label: '@ui.name' }).text(80).required();
general.fieldset(set =>
{
set.field('email').text(120).required();
set.field('passwordHash').when(x => x.id).passwordHash().required();
});
general.field('languageId').culturePicker().required();
general.field('avatarId').image();
permissions.field('claims', { hideLabel: true }).custom(Permissions);
export default editor;
@@ -1,16 +0,0 @@
import countries from './countries.js';
import languages from './languages.js';
import translations from './translations.js';
import users from './users.js';
import mails from './mails.js';
import recyclebin from './recyclebin.js';
export default {
countries,
languages,
translations,
users,
mails,
recyclebin
};
+14 -21
View File
@@ -6,24 +6,23 @@ using System.Security.Claims;
namespace zero.Identity;
public class RavenRoleStore<TRole> : IRoleStore<TRole>, IRoleClaimStore<TRole>
where TRole : ZeroIdentityRole
public class RavenRoleStore<TRole> :
EntityStore<TRole>,
IRoleStore<TRole>,
IRoleClaimStore<TRole>
where TRole : ZeroIdentityRole, new()
{
protected IZeroStore Store { get; private set; }
protected IZeroOptions Options { get; private set; }
protected IdentityErrorDescriber ErrorDescriber { get; private set; }
protected bool Global { get; private set; }
public RavenRoleStore(IZeroStore store, IZeroOptions options, IdentityErrorDescriber describer = null, bool global = false)
public RavenRoleStore(IStoreContext storeContext, IdentityErrorDescriber describer = null, bool global = false) : base(storeContext)
{
Store = store;
Options = options;
ErrorDescriber = describer ?? new IdentityErrorDescriber();
Global = global;
Config.Database = global ? Options.For<RavenOptions>().Database : null;
Config.IncludeInactive = true;
ErrorDescriber = describer ?? new IdentityErrorDescriber();
}
@@ -36,9 +35,7 @@ public class RavenRoleStore<TRole> : IRoleStore<TRole>, IRoleClaimStore<TRole>
/// <inheritdoc/>
public async Task<IdentityResult> CreateAsync(TRole role, CancellationToken cancellationToken)
{
IZeroDocumentSession session = Store.Session(Global);
await session.StoreAsync(role, cancellationToken);
await session.SaveChangesAsync(cancellationToken);
await Create(role);
return IdentityResult.Success;
}
@@ -48,9 +45,7 @@ public class RavenRoleStore<TRole> : IRoleStore<TRole>, IRoleClaimStore<TRole>
{
try
{
IZeroDocumentSession session = Store.Session(Global);
await session.StoreAsync(role, cancellationToken);
await session.SaveChangesAsync(cancellationToken);
await Update(role);
}
catch (ConcurrencyException)
{
@@ -65,9 +60,7 @@ public class RavenRoleStore<TRole> : IRoleStore<TRole>, IRoleClaimStore<TRole>
{
try
{
IZeroDocumentSession session = Store.Session(Global);
session.Delete(role);
await session.SaveChangesAsync(cancellationToken);
await Delete(role);
}
catch (ConcurrencyException)
{
@@ -107,14 +100,14 @@ public class RavenRoleStore<TRole> : IRoleStore<TRole>, IRoleClaimStore<TRole>
/// <inheritdoc/>
public async Task<TRole> FindByIdAsync(string roleId, CancellationToken cancellationToken)
{
return await ScopeQuery(Store.Session(Global).Query<TRole>()).FirstOrDefaultAsync(x => x.Id == roleId, cancellationToken);
return await ScopeQuery(Session.Query<TRole>()).FirstOrDefaultAsync(x => x.Id == roleId, cancellationToken);
}
/// <inheritdoc/>
public async Task<TRole> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
{
return await ScopeQuery(Store.Session(Global).Query<TRole>()).FirstOrDefaultAsync(x => x.Name == normalizedRoleName, cancellationToken);
return await ScopeQuery(Session.Query<TRole>()).FirstOrDefaultAsync(x => x.Name == normalizedRoleName, cancellationToken);
}
+7 -7
View File
@@ -3,25 +3,25 @@
namespace zero.Identity;
public class RavenCoreRoleStore<TRole> : RavenRoleStore<TRole>
where TRole : ZeroIdentityRole
where TRole : ZeroIdentityRole, new()
{
public RavenCoreRoleStore(IZeroStore store, IZeroOptions options, IdentityErrorDescriber describer = null) : base(store, options, describer, true) { }
public RavenCoreRoleStore(IStoreContext context, IdentityErrorDescriber describer = null) : base(context, describer, true) { }
}
public class RavenCoreUserStore<TUser> : RavenUserStore<TUser>
where TUser : ZeroIdentityUser
where TUser : ZeroIdentityUser, new()
{
public RavenCoreUserStore(IZeroStore store, IZeroOptions options) : base(store, options, true) { }
public RavenCoreUserStore(IStoreContext context) : base(context, true) { }
}
public class RavenCoreUserStore<TUser, TRole> : RavenUserStore<TUser, TRole>
where TUser : ZeroIdentityUser
where TRole : ZeroIdentityRole
where TUser : ZeroIdentityUser, new()
where TRole : ZeroIdentityRole, new()
{
public RavenCoreUserStore(IZeroStore store, IZeroOptions options) : base(store, options, true) { }
public RavenCoreUserStore(IStoreContext context) : base(context, true) { }
}
+23 -47
View File
@@ -1,12 +1,13 @@
using Microsoft.AspNetCore.Identity;
using Raven.Client.Documents;
using Raven.Client.Documents.Linq;
using Raven.Client.Documents.Session;
using System.Security.Claims;
namespace zero.Identity;
public partial class RavenUserStore<TUser> :
public partial class RavenUserStore<TUser> :
EntityStore<TUser>,
IUserStore<TUser>,
IUserEmailStore<TUser>,
IUserLockoutStore<TUser>,
@@ -14,20 +15,15 @@ public partial class RavenUserStore<TUser> :
IUserClaimStore<TUser>,
IUserSecurityStampStore<TUser>,
IProtectedUserStore<TUser>
where TUser : ZeroIdentityUser
where TUser : ZeroIdentityUser, new()
{
protected IZeroStore Store { get; private set; }
protected IZeroOptions Options { get; private set; }
protected bool Global { get; private set; }
public RavenUserStore(IZeroStore store, IZeroOptions options, bool global = false)
public RavenUserStore(IStoreContext storeContext, bool global = false) : base(storeContext)
{
Store = store;
Options = options;
Global = global;
Global = global;
Config.Database = global ? Options.For<RavenOptions>().Database : null;
Config.IncludeInactive = true;
}
@@ -45,9 +41,9 @@ public partial class RavenUserStore<TUser> :
/// <summary>
/// Whether an email is already reserved
/// </summary>
protected virtual async Task<bool> IsEmailReserved(IAsyncDocumentSession session, TUser user, CancellationToken cancellationToken = default)
protected virtual async Task<bool> IsEmailReserved(TUser user, CancellationToken cancellationToken = default)
{
TUser existingUser = await ScopeQuery(session.Query<TUser>()).FirstOrDefaultAsync(x => x.Email == user.Email, cancellationToken);
TUser existingUser = await ScopeQuery(Session.Query<TUser>()).FirstOrDefaultAsync(x => x.Email == user.Email, cancellationToken);
return existingUser != null && existingUser.Id != user.Id;
}
@@ -55,9 +51,7 @@ public partial class RavenUserStore<TUser> :
/// <inheritdoc />
public async Task<IdentityResult> CreateAsync(TUser user, CancellationToken cancellationToken)
{
IZeroDocumentSession session = Store.Session(Global);
if (await IsEmailReserved(session, user, cancellationToken))
if (await IsEmailReserved(user, cancellationToken))
{
return IdentityResult.Failed(new IdentityError
{
@@ -66,9 +60,7 @@ public partial class RavenUserStore<TUser> :
});
}
await session.StoreAsync(user, cancellationToken);
await session.SaveChangesAsync(cancellationToken);
Result<TUser> result = await Create(user);
return IdentityResult.Success;
}
@@ -76,13 +68,7 @@ public partial class RavenUserStore<TUser> :
/// <inheritdoc />
public async Task<IdentityResult> DeleteAsync(TUser user, CancellationToken cancellationToken)
{
IZeroDocumentSession session = Store.Session(Global);
TUser source = await session.LoadAsync<TUser>(user.Id, cancellationToken);
session.Delete(source);
await session.SaveChangesAsync(cancellationToken);
Result result = await Delete(user);
return IdentityResult.Success;
}
@@ -90,9 +76,7 @@ public partial class RavenUserStore<TUser> :
/// <inheritdoc />
public async Task<IdentityResult> UpdateAsync(TUser user, CancellationToken cancellationToken)
{
using IZeroDocumentSession session = Store.Session(Global);
using IAsyncDocumentSession newSession = Store.Session(Global, null, ZeroSessionResolution.Create, new SessionOptions() { NoCaching = true });
TUser source = await newSession.LoadAsync<TUser>(user.Id, cancellationToken);
TUser source = await Load(user.Id);
if (source == null)
{
@@ -103,7 +87,7 @@ public partial class RavenUserStore<TUser> :
});
}
if (source.Email != user.Email && await IsEmailReserved(Store.Session(Global), user, cancellationToken))
if (source.Email != user.Email && await IsEmailReserved(user, cancellationToken))
{
return IdentityResult.Failed(new IdentityError
{
@@ -112,8 +96,7 @@ public partial class RavenUserStore<TUser> :
});
}
await session.StoreAsync(user, cancellationToken);
await session.SaveChangesAsync(cancellationToken);
await Update(user);
return IdentityResult.Success;
}
@@ -122,14 +105,14 @@ public partial class RavenUserStore<TUser> :
/// <inheritdoc />
public async Task<TUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
{
return await ScopeQuery(Store.Session(Global).Query<TUser>()).FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
return await ScopeQuery(Session.Query<TUser>()).FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
}
/// <inheritdoc />
public async Task<TUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
{
return await ScopeQuery(Store.Session(Global).Query<TUser>()).FirstOrDefaultAsync(x => x.Username == normalizedUserName, cancellationToken);
return await ScopeQuery(Session.Query<TUser>()).FirstOrDefaultAsync(x => x.Username == normalizedUserName, cancellationToken);
}
@@ -196,7 +179,7 @@ public partial class RavenUserStore<TUser> :
/// <inheritdoc />
public async Task<TUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken)
{
return await Store.Session(Global).Query<TUser>().FirstOrDefaultAsync(x => x.Email == normalizedEmail, cancellationToken);
return await Session.Query<TUser>().FirstOrDefaultAsync(x => x.Email == normalizedEmail, cancellationToken);
}
@@ -320,10 +303,8 @@ public partial class RavenUserStore<TUser> :
/// <inheritdoc />
public async Task AddClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken)
{
IZeroDocumentSession session = Store.Session(Global);
user.Claims.AddRange(claims.Select(claim => new UserClaim(claim)));
await session.StoreAsync(user, cancellationToken);
await session.SaveChangesAsync(cancellationToken);
await Update(user);
}
@@ -338,35 +319,30 @@ public partial class RavenUserStore<TUser> :
public async Task<IList<TUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken)
{
UserClaim userClaim = new(claim);
return await ScopeQuery(Store.Session(Global).Query<TUser>()).Where(x => x.Claims.Any(c => c.Type == userClaim.Type && c.Value == userClaim.Value)).ToListAsync(token: cancellationToken);
return await ScopeQuery(Session.Query<TUser>()).Where(x => x.Claims.Any(c => c.Type == userClaim.Type && c.Value == userClaim.Value)).ToListAsync(token: cancellationToken);
}
/// <inheritdoc />
public async Task RemoveClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken)
{
IZeroDocumentSession session = Store.Session(Global);
IEnumerable<UserClaim> userClaims = claims.Select(c => new UserClaim(c)).ToList();
user.Claims = user.Claims.Except(userClaims, new UserClaimComparer()).ToList();
await session.StoreAsync(user, cancellationToken);
await session.SaveChangesAsync(cancellationToken);
await Update(user);
}
/// <inheritdoc />
public async Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken)
{
IZeroDocumentSession session = Store.Session(Global);
UserClaim userClaim = new(claim);
UserClaim newUserClaim = new(newClaim);
user.Claims.Remove(userClaim);
user.Claims.Add(newUserClaim);
await session.StoreAsync(user, cancellationToken);
await session.SaveChangesAsync(cancellationToken);
await Update(user);
}
}
@@ -9,10 +9,10 @@ namespace zero.Identity;
public partial class RavenUserStore<TUser, TRole> : RavenUserStore<TUser>,
IUserRoleStore<TUser>
where TUser : ZeroIdentityUser
where TRole : ZeroIdentityRole
where TUser : ZeroIdentityUser, new()
where TRole : ZeroIdentityRole, new()
{
public RavenUserStore(IZeroStore store, IZeroOptions options, bool global = false) : base(store, options, global) { }
public RavenUserStore(IStoreContext context, bool global = false) : base(context, global) { }
/// <inheritdoc />
@@ -33,7 +33,7 @@ public partial class RavenUserStore<TUser, TRole> : RavenUserStore<TUser>,
/// <inheritdoc />
public async Task<IList<TUser>> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken)
{
return await ScopeQuery(Store.Session(Global).Query<TUser>()).Where(x => roleName.In(x.RoleIds)).ToListAsync();
return await ScopeQuery(Session.Query<TUser>()).Where(x => roleName.In(x.RoleIds)).ToListAsync();
}
+1 -1
View File
@@ -17,7 +17,7 @@ public class UserService : IUserService
UserManager = userManager;
Operations = new StoreOperations(store.Context, store.Interceptors, store.Options, new()
{
IncludeInactive = false,
IncludeInactive = true,
Database = store.Options.For<RavenOptions>().Database
});
Context = context;
+4 -4
View File
@@ -7,8 +7,8 @@ namespace zero.Identity;
public static class ZeroIdentityExtensions
{
public static IdentityBuilder AddZeroIdentity<TUser, TRole>(this IServiceCollection services)
where TUser : ZeroIdentityUser
where TRole : ZeroIdentityRole
where TUser : ZeroIdentityUser, new()
where TRole : ZeroIdentityRole, new()
{
services.AddZeroIdentityCore<TUser>();
@@ -29,7 +29,7 @@ public static class ZeroIdentityExtensions
public static IdentityBuilder AddZeroIdentity<TUser>(this IServiceCollection services)
where TUser : ZeroIdentityUser
where TUser : ZeroIdentityUser, new()
{
services.AddZeroIdentityCore<TUser>();
@@ -45,7 +45,7 @@ public static class ZeroIdentityExtensions
static IServiceCollection AddZeroIdentityCore<TUser>(this IServiceCollection services)
where TUser : ZeroIdentityUser
where TUser : ZeroIdentityUser, new()
{
services.AddHttpContextAccessor();
services.AddOptions();
+1 -1
View File
@@ -8,7 +8,7 @@ namespace zero.Stores;
public partial class StoreOperations : IStoreOperations
{
/// <inheritdoc />
public IZeroDocumentSession Session => Context.Store.Session(_overrideDatabase);
public IZeroDocumentSession Session => Context.Store.Session(_overrideDatabase ?? Config.Database);
protected record OperationOptions(bool IncludeInactive);
+1 -2
View File
@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using zero.Routing;
namespace zero.Web;
@@ -8,6 +7,6 @@ public class ZeroFallbackController : ZeroController<PageRoute>
{
public IActionResult Index()
{
return Json(new { Application, Route }, new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.None });
return Json(new { Application, Route });
}
}