fix(plugin-authentication): user creation

CB-295
This commit is contained in:
Wroud
2020-09-09 19:01:42 +03:00
parent 6fed738269
commit 97f3a944a5
9 changed files with 135 additions and 91 deletions
@@ -26,7 +26,6 @@ type AdminUserNew = AdminUserInfo & { [NEW_USER_SYMBOL]: boolean }
type UserCreateOptions = {
userId: string;
newId?: string;
roles: string[];
credentials: Record<string, any>;
grantedConnections: string[];
@@ -59,22 +58,6 @@ export class UsersResource extends CachedMapResource<string, AdminUserInfo> {
return this.data.has(id);
}
addNew() {
const user = {
userId: `new-${uuid()}`,
grantedRoles: [],
grantedConnections: [],
configurationParameters: {},
metaParameters: {},
[NEW_USER_SYMBOL]: true,
} as AdminUserNew;
this.data.set(user.userId, user);
this.markUpdated(user.userId);
return user;
}
async loadConnections(userId: string): Promise<AdminConnectionGrantInfo[]> {
if (this.isNew(userId)) {
return [];
@@ -90,15 +73,10 @@ export class UsersResource extends CachedMapResource<string, AdminUserInfo> {
}
async create({
userId, newId, roles, credentials, grantedConnections,
userId, roles, credentials, grantedConnections,
}: UserCreateOptions): Promise<AdminUserInfo> {
const { user } = await this.graphQLService.gql.createUser({ userId });
if (newId) {
this.data.delete(newId);
}
this.set(userId, user as AdminUserInfo);
try {
await this.updateCredentials(userId, credentials);
for (const roleId of roles) {
@@ -106,6 +84,8 @@ export class UsersResource extends CachedMapResource<string, AdminUserInfo> {
}
await this.setConnections(userId, grantedConnections);
const user = await this.refresh(userId)as AdminUserNew;
user[NEW_USER_SYMBOL] = true;
} catch (exception) {
this.delete(userId);
throw exception;
@@ -33,12 +33,6 @@ const styles = composes(
display: flex;
flex-direction: column;
}
ErrorMessage {
position: sticky;
bottom: 0;
padding: 8px 24px;
}
`
);
@@ -28,13 +28,13 @@ const styles = composes(
css`
title-bar {
composes: theme-typography--headline6 from global;
padding: 0 16px;
box-sizing: border-box;
padding: 16px 24px;
border-top: solid 1px;
align-items: center;
height: 48px;
display: flex;
font-weight: 400;
flex: 1;
flex: auto 0 0;
}
`
);
@@ -47,7 +47,7 @@ export function CreateUser({
return styled(useStyles(styles))(
<user-create as='div'>
<title-bar as='div'><Translate token='authentication_administration_user_connections_user_add'/></title-bar>
<UserForm userId={user.userId} onCancel={onCancel} />
<UserForm user={user} onCancel={onCancel} />
</user-create>
);
}
@@ -21,6 +21,7 @@ import {
} from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { AdminUserInfo } from '@cloudbeaver/core-sdk';
import { useStyles, composes } from '@cloudbeaver/core-theming';
import { GrantedConnections } from './GrantedConnections';
@@ -53,10 +54,6 @@ const styles = composes(
}
`,
css`
box {
padding: 24px;
}
SubmittingForm {
flex: 1;
display: flex;
@@ -128,16 +125,18 @@ const styles = composes(
);
type Props = {
userId: string;
user: AdminUserInfo;
editing?: boolean;
onCancel: () => void;
}
export const UserForm = observer(function UserForm({
userId,
user,
editing = false,
onCancel,
}: Props) {
const translate = useTranslate();
const controller = useController(UserFormController, userId, onCancel);
const controller = useController(UserFormController, user, editing, onCancel);
const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild: true });
const handleLoginChange = useCallback(
@@ -182,7 +181,7 @@ export const UserForm = observer(function UserForm({
mod={['unelevated']}
onClick={controller.save}
>
{translate(controller.isNew ? 'ui_processing_create' : 'ui_processing_save')}
{translate(!editing ? 'ui_processing_create' : 'ui_processing_save')}
</Button>
</TabList>
<content-box as='div'>
@@ -199,7 +198,7 @@ export const UserForm = observer(function UserForm({
name='login'
value={controller.credentials.login}
onChange={handleLoginChange}
disabled={!controller.isNew || controller.isSaving}
disabled={editing || controller.isSaving}
mod='surface'
required
>
@@ -31,10 +31,6 @@ export class UserFormController implements IInitializableController, IDestructib
roles: new Map<string, boolean>(),
};
@computed get isNew() {
return this.usersResource.isNew(this.userId);
}
@computed get connections() {
return Array.from(this.connectionsResource.data.values())
.filter(connection => !this.connectionsResource.isNew(connection.id));
@@ -44,16 +40,14 @@ export class UserFormController implements IInitializableController, IDestructib
return Array.from(this.rolesManagerService.roles.data.values());
}
@computed get user(): AdminUserInfo {
return this.usersResource.get(this.userId)!;
}
user!: AdminUserInfo;
readonly error = new GQLErrorCatcher();
private isDistructed = false;
private userId!: string;
private connectionAccessChanged = false;
private connectionAccessLoaded = false;
private collapse!: () => void;
private editing!: boolean;
constructor(
private notificationService: NotificationService,
@@ -64,8 +58,9 @@ export class UserFormController implements IInitializableController, IDestructib
private dbDriverResource: DBDriverResource
) { }
init(id: string, collapse: () => void) {
this.userId = id;
init(user: AdminUserInfo, editing: boolean, collapse: () => void) {
this.user = user;
this.editing = editing;
this.collapse = collapse;
this.loadRoles();
}
@@ -84,10 +79,9 @@ export class UserFormController implements IInitializableController, IDestructib
this.isSaving = true;
try {
if (this.isNew) {
if (!this.editing) {
await this.usersResource.create({
userId: this.credentials.login,
newId: this.userId,
credentials: { password: this.credentials.password },
roles: this.getGrantedRoles(),
grantedConnections: this.getGrantedConnections(),
@@ -103,12 +97,9 @@ export class UserFormController implements IInitializableController, IDestructib
await this.usersResource.refresh(this.user.userId);
this.notificationService.logInfo({ title: 'authentication_administration_user_updated' });
}
this.connectionAccessLoaded = false;
await this.loadConnectionsAccess();
} catch (exception) {
if (!this.error.catch(exception) || this.isDistructed) {
if (this.isNew) {
if (!this.editing) {
this.notificationService.logException(exception, 'Error creating new user');
} else {
this.notificationService.logException(exception, 'Error saving user');
@@ -134,12 +125,14 @@ export class UserFormController implements IInitializableController, IDestructib
this.isLoading = true;
try {
this.grantedConnections = await this.usersResource.loadConnections(this.userId);
if (this.editing) {
this.grantedConnections = await this.usersResource.loadConnections(this.user.userId);
this.selectedConnections.clear();
for (const connection of this.grantedConnections) {
if (connection.subjectType !== AdminSubjectType.Role) {
this.selectedConnections.set(connection.connectionId, true);
this.selectedConnections.clear();
for (const connection of this.grantedConnections) {
if (connection.subjectType !== AdminSubjectType.Role) {
this.selectedConnections.set(connection.connectionId, true);
}
}
}
this.connectionAccessLoaded = true;
@@ -161,12 +154,12 @@ export class UserFormController implements IInitializableController, IDestructib
return;
}
if (!this.credentials.password && this.isNew) {
if (!this.credentials.password && !this.editing) {
this.notificationService.logError({ title: 'authentication_user_password_not_set' });
return;
}
if (!this.credentials.password && this.isNew) {
if (!this.credentials.password && !this.editing) {
this.notificationService.logError({ title: 'authentication_user_password_not_set' });
return;
}
@@ -190,7 +183,7 @@ export class UserFormController implements IInitializableController, IDestructib
if (!this.user.grantedRoles.includes(roleId)) {
await this.usersResource.grantRole(this.user.userId, roleId, true);
}
} else if (!this.isNew) {
} else {
await this.usersResource.revokeRole(this.user.userId, roleId, true);
}
}
@@ -215,8 +208,10 @@ export class UserFormController implements IInitializableController, IDestructib
if (!this.connectionAccessChanged) {
return;
}
await this.usersResource.setConnections(this.userId, this.getGrantedConnections());
await this.usersResource.setConnections(this.user.userId, this.getGrantedConnections());
this.connectionAccessChanged = false;
this.connectionAccessLoaded = false;
await this.loadConnectionsAccess();
}
private async loadRoles() {
@@ -232,11 +227,7 @@ export class UserFormController implements IInitializableController, IDestructib
private async loadUser() {
try {
await this.usersResource.load(this.userId);
if (!this.isNew) {
this.credentials.login = this.user.userId;
}
this.credentials.login = this.user.userId;
this.credentials.roles = new Map(this.user.grantedRoles.map(roleId => ([roleId, true])));
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load user');
@@ -13,7 +13,7 @@ import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialog } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
import { GQLErrorCatcher, resourceKeyList } from '@cloudbeaver/core-sdk';
import { GQLErrorCatcher, resourceKeyList, AdminUserInfo } from '@cloudbeaver/core-sdk';
import { UsersAdministrationNavigationService } from './UsersAdministrationNavigationService';
@@ -26,10 +26,9 @@ export class UsersAdministrationController {
@computed get users() {
return Array.from(this.usersResource.data.values())
.filter(user => !this.usersResource.isNew(user.userId))
.sort((a, b) => {
if (this.usersResource.isNew(a.userId) === this.usersResource.isNew(b.userId)) {
return 0;
return a.userId.localeCompare(b.userId);
}
if (this.usersResource.isNew(a.userId)) {
return -1;
@@ -38,10 +37,7 @@ export class UsersAdministrationController {
});
}
@computed get creatingUser() {
return Array.from(this.usersResource.data.values())
.find(user => this.usersResource.isNew(user.userId));
}
@observable creatingUser: AdminUserInfo | null = null;
get isLoading() {
return this.usersResource.isLoading() || this.isDeleting;
@@ -55,7 +51,13 @@ export class UsersAdministrationController {
) { }
create = () => {
this.usersResource.addNew();
this.creatingUser = {
userId: '',
grantedRoles: [],
grantedConnections: [],
configurationParameters: {},
metaParameters: {},
} as AdminUserInfo;
this.usersAdministrationNavigationService.navToAdd();
}
@@ -64,7 +66,7 @@ export class UsersAdministrationController {
return;
}
this.usersResource.delete(this.creatingUser.userId);
this.creatingUser = null;
this.usersAdministrationNavigationService.navToRoot();
}
@@ -7,14 +7,11 @@
*/
import { observer } from 'mobx-react';
import styled, { css, use } from 'reshadow';
import styled, { css } from 'reshadow';
import { UsersResource } from '@cloudbeaver/core-authentication';
import {
TableItem, TableColumnValue, TableItemSelect, TableItemExpand
} from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { AdminUserInfo } from '@cloudbeaver/core-sdk';
import { useStyles } from '@cloudbeaver/core-theming';
@@ -31,9 +28,6 @@ type Props = {
}
export const User = observer(function User({ user }: Props) {
const translate = useTranslate();
const usersResource = useService(UsersResource);
const isNew = usersResource.isNew(user.userId);
return styled(useStyles(styles))(
<TableItem item={user.userId} expandElement={UserEdit}>
@@ -43,9 +37,9 @@ export const User = observer(function User({ user }: Props) {
<TableColumnValue centerContent flex expand>
<TableItemExpand />
</TableColumnValue>
<TableColumnValue expand>{isNew ? translate('authentication_administration_user_connections_user_new') : user.userId}</TableColumnValue>
<TableColumnValue expand>{user.userId}</TableColumnValue>
<TableColumnValue>{user.grantedRoles.join(', ')}</TableColumnValue>
<TableColumnValue align='right'>{isNew && <tag as='div' {...use({ mod: 'positive' })}>{translate('ui_tag_new')}</tag>}</TableColumnValue>
<TableColumnValue></TableColumnValue>
</TableItem>
);
});
@@ -8,12 +8,34 @@
import { observer } from 'mobx-react';
import { useContext, useCallback } from 'react';
import styled, { css } from 'reshadow';
import {
TableContext,
} from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { UserForm } from '../UserForm/UserForm';
import { UserEditController } from './UserEditController';
const styles = composes(
css`
box {
composes: theme-background-secondary theme-text-on-secondary from global;
}
`,
css`
box {
box-sizing: border-box;
padding: 24px;
min-height: 320px;
max-height: 500px;
display: flex;
flex-direction: column;
}
`
);
type Props = {
item: string;
@@ -22,8 +44,15 @@ type Props = {
export const UserEdit = observer(function UserEdit({
item,
}: Props) {
const controller = useController(UserEditController, item);
const tableContext = useContext(TableContext);
const collapse = useCallback(() => tableContext?.setItemExpand(item, false), [tableContext]);
return <UserForm userId={item} onCancel={collapse} />;
return styled(useStyles(styles))(
<box as='div'>
{controller.user && (
<UserForm user={controller.user} onCancel={collapse} editing/>
)}
</box>
);
});
@@ -0,0 +1,55 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observable, computed } from 'mobx';
import { UsersResource } from '@cloudbeaver/core-authentication';
import {
injectable, IInitializableController, IDestructibleController
} from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { GQLErrorCatcher, AdminUserInfo } from '@cloudbeaver/core-sdk';
@injectable()
export class UserEditController
implements IInitializableController, IDestructibleController {
@observable isLoading = true;
@observable user: AdminUserInfo | null = null;
@computed get isDisabled() {
return this.isLoading;
}
userId!: string;
readonly error = new GQLErrorCatcher();
constructor(
private notificationService: NotificationService,
private usersResource: UsersResource,
) { }
init(id: string) {
this.userId = id;
this.loadUser();
}
destruct(): void { }
private async loadUser() {
this.isLoading = true;
try {
// we create a copy to protect the current value from mutation
this.user = JSON.parse(JSON.stringify(await this.usersResource.load(this.userId)));
} catch (exception) {
this.notificationService.logException(exception, `Can't load user ${this.userId}`);
} finally {
this.isLoading = false;
}
}
}