mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-13 23:35:39 +01:00
add user edit page
This commit is contained in:
@@ -6,6 +6,7 @@ import { PicsurConfigModule } from '../../config/config.module';
|
||||
import { EUserBackend } from '../../models/entities/user.entity';
|
||||
import { RolesModule } from '../roledb/roledb.module';
|
||||
import { UsersService } from './userdb.service';
|
||||
import { UserRolesService } from './userrolesdb.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -13,14 +14,15 @@ import { UsersService } from './userdb.service';
|
||||
RolesModule,
|
||||
TypeOrmModule.forFeature([EUserBackend]),
|
||||
],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
providers: [UsersService, UserRolesService],
|
||||
exports: [UsersService, UserRolesService],
|
||||
})
|
||||
export class UsersModule implements OnModuleInit {
|
||||
private readonly logger = new Logger('UsersModule');
|
||||
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private userRolesService: UserRolesService,
|
||||
private authConfigService: AuthConfigService,
|
||||
) {}
|
||||
|
||||
@@ -44,7 +46,7 @@ export class UsersModule implements OnModuleInit {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.usersService.addRoles(newUser, ['admin']);
|
||||
const result = await this.userRolesService.addRoles(newUser, ['admin']);
|
||||
if (HasFailed(result)) {
|
||||
this.logger.error(
|
||||
`Failed to make admin user "${username}" because: ${result.getReason()}`,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { Permissions } from 'picsur-shared/dist/dto/permissions';
|
||||
import { PermanentRolesList, Roles } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import {
|
||||
AsyncFailable,
|
||||
@@ -16,6 +15,8 @@ import { EUserBackend } from '../../models/entities/user.entity';
|
||||
import { GetCols } from '../collectionutils';
|
||||
import { RolesService } from '../roledb/roledb.service';
|
||||
|
||||
const BCryptStrength = 12;
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
private readonly logger = new Logger('UsersService');
|
||||
@@ -35,7 +36,7 @@ export class UsersService {
|
||||
): AsyncFailable<EUserBackend> {
|
||||
if (await this.exists(username)) return Fail('User already exists');
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
const hashedPassword = await bcrypt.hash(password, BCryptStrength);
|
||||
|
||||
let user = new EUserBackend();
|
||||
user.username = username;
|
||||
@@ -65,6 +66,52 @@ export class UsersService {
|
||||
}
|
||||
}
|
||||
|
||||
// Updating
|
||||
|
||||
public async setRoles(
|
||||
user: string | EUserBackend,
|
||||
roles: Roles,
|
||||
): AsyncFailable<EUserBackend> {
|
||||
const userToModify = await this.resolve(user);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
|
||||
const rolesToKeep = userToModify.roles.filter((role) =>
|
||||
PermanentRolesList.includes(role),
|
||||
);
|
||||
const rolesToAdd = roles.filter(
|
||||
(role) => !PermanentRolesList.includes(role),
|
||||
);
|
||||
|
||||
const newRoles = [...new Set([...rolesToKeep, ...rolesToAdd])];
|
||||
|
||||
userToModify.roles = newRoles;
|
||||
|
||||
try {
|
||||
return await this.usersRepository.save(userToModify);
|
||||
} catch (e: any) {
|
||||
return Fail(e?.message);
|
||||
}
|
||||
}
|
||||
|
||||
public async updatePassword(
|
||||
user: string | EUserBackend,
|
||||
password: string,
|
||||
): AsyncFailable<EUserBackend> {
|
||||
const userToModify = await this.resolve(user);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, BCryptStrength);
|
||||
|
||||
userToModify.password = hashedPassword;
|
||||
|
||||
try {
|
||||
const fullUser = await this.usersRepository.save(userToModify);
|
||||
return plainToClass(EUserBackend, fullUser);
|
||||
} catch (e: any) {
|
||||
return Fail(e?.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication
|
||||
|
||||
async authenticate(
|
||||
@@ -80,62 +127,6 @@ export class UsersService {
|
||||
return await this.findOne(username);
|
||||
}
|
||||
|
||||
// Permissions and roles
|
||||
|
||||
public async getPermissions(
|
||||
user: string | EUserBackend,
|
||||
): AsyncFailable<Permissions> {
|
||||
const userToModify = await this.resolve(user);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
|
||||
return await this.rolesService.getPermissions(userToModify.roles);
|
||||
}
|
||||
|
||||
public async addRoles(
|
||||
user: string | EUserBackend,
|
||||
roles: Roles,
|
||||
): AsyncFailable<EUserBackend> {
|
||||
const userToModify = await this.resolve(user);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
|
||||
const newRoles = [...new Set([...userToModify.roles, ...roles])];
|
||||
|
||||
return this.setRoles(userToModify, newRoles);
|
||||
}
|
||||
|
||||
public async removeRoles(
|
||||
user: string | EUserBackend,
|
||||
roles: Roles,
|
||||
): AsyncFailable<EUserBackend> {
|
||||
const userToModify = await this.resolve(user);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
|
||||
const newRoles = userToModify.roles.filter((role) => !roles.includes(role));
|
||||
|
||||
return this.setRoles(userToModify, newRoles);
|
||||
}
|
||||
|
||||
public async setRoles(
|
||||
user: string | EUserBackend,
|
||||
roles: Roles,
|
||||
): AsyncFailable<EUserBackend> {
|
||||
const userToModify = await this.resolve(user);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
|
||||
const rolesToKeep = userToModify.roles.filter((role) =>
|
||||
PermanentRolesList.includes(role),
|
||||
);
|
||||
const newRoles = [...new Set([...rolesToKeep, ...roles])];
|
||||
|
||||
userToModify.roles = newRoles;
|
||||
|
||||
try {
|
||||
return await this.usersRepository.save(userToModify);
|
||||
} catch (e: any) {
|
||||
return Fail(e?.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Listing
|
||||
|
||||
public async findOne<B extends true | undefined = undefined>(
|
||||
@@ -182,7 +173,7 @@ export class UsersService {
|
||||
|
||||
// Internal resolver
|
||||
|
||||
private async resolve(
|
||||
public async resolve(
|
||||
user: string | EUserBackend,
|
||||
): AsyncFailable<EUserBackend> {
|
||||
if (typeof user === 'string') {
|
||||
|
||||
47
backend/src/collections/userdb/userrolesdb.service.ts
Normal file
47
backend/src/collections/userdb/userrolesdb.service.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Permissions } from 'picsur-shared/dist/dto/permissions';
|
||||
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { EUserBackend } from '../../models/entities/user.entity';
|
||||
import { RolesService } from '../roledb/roledb.service';
|
||||
import { UsersService } from './userdb.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserRolesService {
|
||||
constructor(private usersService: UsersService, private rolesService: RolesService){}
|
||||
|
||||
// Permissions and roles
|
||||
|
||||
public async getPermissions(
|
||||
user: string | EUserBackend,
|
||||
): AsyncFailable<Permissions> {
|
||||
const userToModify = await this.usersService.resolve(user);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
|
||||
return await this.rolesService.getPermissions(userToModify.roles);
|
||||
}
|
||||
|
||||
public async addRoles(
|
||||
user: string | EUserBackend,
|
||||
roles: Roles,
|
||||
): AsyncFailable<EUserBackend> {
|
||||
const userToModify = await this.usersService.resolve(user);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
|
||||
const newRoles = [...new Set([...userToModify.roles, ...roles])];
|
||||
|
||||
return this.usersService.setRoles(userToModify, newRoles);
|
||||
}
|
||||
|
||||
public async removeRoles(
|
||||
user: string | EUserBackend,
|
||||
roles: Roles,
|
||||
): AsyncFailable<EUserBackend> {
|
||||
const userToModify = await this.usersService.resolve(user);
|
||||
if (HasFailed(userToModify)) return userToModify;
|
||||
|
||||
const newRoles = userToModify.roles.filter((role) => !roles.includes(role));
|
||||
|
||||
return this.usersService.setRoles(userToModify, newRoles);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import * as multipart from 'fastify-multipart';
|
||||
import { ValidateOptions } from 'picsur-shared/dist/util/validate';
|
||||
import { AppModule } from './app.module';
|
||||
import { UsersService } from './collections/userdb/userdb.service';
|
||||
import { UserRolesService } from './collections/userdb/userrolesdb.service';
|
||||
import { HostConfigService } from './config/host.config.service';
|
||||
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
|
||||
import { SuccessInterceptor } from './layers/success/success.interceptor';
|
||||
@@ -15,11 +16,12 @@ import { PicsurLoggerService } from './logger/logger.service';
|
||||
import { MainAuthGuard } from './managers/auth/guards/main.guard';
|
||||
|
||||
async function bootstrap() {
|
||||
// Create fasify
|
||||
const fastifyAdapter = new FastifyAdapter();
|
||||
|
||||
// TODO: generic error messages
|
||||
fastifyAdapter.register(multipart as any);
|
||||
|
||||
// Create nest app
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
fastifyAdapter,
|
||||
@@ -27,15 +29,23 @@ async function bootstrap() {
|
||||
bufferLogs: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Configure nest app
|
||||
app.useGlobalFilters(new MainExceptionFilter());
|
||||
app.useGlobalInterceptors(new SuccessInterceptor());
|
||||
app.useGlobalPipes(new ValidationPipe(ValidateOptions));
|
||||
app.useGlobalGuards(
|
||||
new MainAuthGuard(app.get(Reflector), app.get(UsersService)),
|
||||
new MainAuthGuard(
|
||||
app.get(Reflector),
|
||||
app.get(UsersService),
|
||||
app.get(UserRolesService),
|
||||
),
|
||||
);
|
||||
|
||||
// Configure logger
|
||||
app.useLogger(app.get(PicsurLoggerService));
|
||||
|
||||
// Start app
|
||||
const hostConfigService = app.get(HostConfigService);
|
||||
await app.listen(hostConfigService.getPort(), hostConfigService.getHost());
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Fail, Failable, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { isPermissionsArray } from 'picsur-shared/dist/util/permissions';
|
||||
import { strictValidate } from 'picsur-shared/dist/util/validate';
|
||||
import { UsersService } from '../../../collections/userdb/userdb.service';
|
||||
import { UserRolesService } from '../../../collections/userdb/userrolesdb.service';
|
||||
import { EUserBackend } from '../../../models/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
@@ -24,6 +25,7 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private usersService: UsersService,
|
||||
private userRolesService: UserRolesService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -45,7 +47,7 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
||||
throw new InternalServerErrorException();
|
||||
}
|
||||
|
||||
const userPermissions = await this.usersService.getPermissions(user);
|
||||
const userPermissions = await this.userRolesService.getPermissions(user);
|
||||
if (HasFailed(userPermissions)) {
|
||||
this.logger.warn('111' + userPermissions.getReason());
|
||||
throw new InternalServerErrorException();
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { Permission } from 'picsur-shared/dist/dto/permissions';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { UsersService } from '../../../collections/userdb/userdb.service';
|
||||
import { UserRolesService } from '../../../collections/userdb/userrolesdb.service';
|
||||
import {
|
||||
NoPermissions,
|
||||
RequiredPermissions,
|
||||
@@ -31,6 +32,7 @@ export class UserController {
|
||||
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private userRolesSerivce: UserRolesService,
|
||||
private authService: AuthManagerService,
|
||||
) {}
|
||||
|
||||
@@ -74,7 +76,7 @@ export class UserController {
|
||||
async refresh(
|
||||
@Request() req: AuthFasityRequest,
|
||||
): Promise<UserMePermissionsResponse> {
|
||||
const permissions = await this.usersService.getPermissions(req.user);
|
||||
const permissions = await this.userRolesSerivce.getPermissions(req.user);
|
||||
if (HasFailed(permissions)) {
|
||||
this.logger.warn(permissions.getReason());
|
||||
throw new InternalServerErrorException('Could not get permissions');
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
UserInfoResponse,
|
||||
UserListRequest,
|
||||
UserListResponse,
|
||||
UserUpdateRolesRequest,
|
||||
UserUpdateRolesResponse
|
||||
UserUpdateRequest,
|
||||
UserUpdateResponse
|
||||
} from 'picsur-shared/dist/dto/api/usermanage.dto';
|
||||
import { Permission } from 'picsur-shared/dist/dto/permissions';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
@@ -63,6 +63,7 @@ export class UserManageController {
|
||||
const user = await this.usersService.create(
|
||||
create.username,
|
||||
create.password,
|
||||
create.roles,
|
||||
);
|
||||
if (HasFailed(user)) {
|
||||
this.logger.warn(user.getReason());
|
||||
@@ -96,20 +97,32 @@ export class UserManageController {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Post('roles')
|
||||
@Post('update')
|
||||
async setPermissions(
|
||||
@Body() body: UserUpdateRolesRequest,
|
||||
): Promise<UserUpdateRolesResponse> {
|
||||
const updatedUser = await this.usersService.setRoles(
|
||||
body.username,
|
||||
body.roles,
|
||||
);
|
||||
@Body() body: UserUpdateRequest,
|
||||
): Promise<UserUpdateResponse> {
|
||||
let user = await this.usersService.findOne(body.username);
|
||||
if (HasFailed(user)) {
|
||||
this.logger.warn(user.getReason());
|
||||
throw new InternalServerErrorException('Could not find user');
|
||||
}
|
||||
|
||||
if (HasFailed(updatedUser)) {
|
||||
this.logger.warn(updatedUser.getReason());
|
||||
if (body.roles) {
|
||||
user = await this.usersService.setRoles(user, body.roles);
|
||||
if (HasFailed(user)) {
|
||||
this.logger.warn(user.getReason());
|
||||
throw new InternalServerErrorException('Could not update user');
|
||||
}
|
||||
}
|
||||
|
||||
return updatedUser;
|
||||
if (body.password) {
|
||||
user = await this.usersService.updatePassword(user, body.password);
|
||||
if (HasFailed(user)) {
|
||||
this.logger.warn(user.getReason());
|
||||
throw new InternalServerErrorException('Could not update user');
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"bootstrap": "^5.1.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.13.2",
|
||||
"fuse.js": "^6.5.3",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"ngx-auto-unsubscribe-decorator": "^1.0.0",
|
||||
"ngx-dropzone": "^3.1.0",
|
||||
|
||||
@@ -69,8 +69,8 @@ export class HeaderComponent implements OnInit {
|
||||
this.router.navigate(['/user/login']);
|
||||
}
|
||||
|
||||
doLogout() {
|
||||
const user = this.userService.logout();
|
||||
async doLogout() {
|
||||
const user = await this.userService.logout();
|
||||
if (HasFailed(user)) {
|
||||
this.utilService.showSnackBar(user.getReason(), SnackBarType.Error);
|
||||
return;
|
||||
|
||||
@@ -12,7 +12,6 @@ function errorsToError(errors: ValidationErrors | null): string {
|
||||
}
|
||||
|
||||
export const UsernameValidators = [
|
||||
Validators.required,
|
||||
Validators.minLength(4),
|
||||
Validators.maxLength(32),
|
||||
Validators.pattern('^[a-zA-Z0-9]+$'),
|
||||
@@ -37,7 +36,6 @@ export const CreateUsernameError = (
|
||||
};
|
||||
|
||||
export const PasswordValidators = [
|
||||
Validators.required,
|
||||
Validators.minLength(4),
|
||||
Validators.maxLength(1024),
|
||||
];
|
||||
|
||||
7
frontend/src/app/models/forms/fulluser.model.ts
Normal file
7
frontend/src/app/models/forms/fulluser.model.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
|
||||
|
||||
export interface FullUserModel {
|
||||
username: string;
|
||||
password: string;
|
||||
roles: Roles;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
PasswordValidators,
|
||||
UsernameValidators
|
||||
} from './default-validators';
|
||||
import { UserPassModel } from './userpass';
|
||||
import { UserPassModel } from './userpass.model';
|
||||
|
||||
export class LoginControl {
|
||||
public username = new FormControl('', UsernameValidators);
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
PasswordValidators,
|
||||
UsernameValidators
|
||||
} from './default-validators';
|
||||
import { UserPassModel } from './userpass';
|
||||
import { UserPassModel } from './userpass.model';
|
||||
|
||||
export class RegisterControl {
|
||||
public username = new FormControl('', UsernameValidators);
|
||||
123
frontend/src/app/models/forms/updateuser.control.ts
Normal file
123
frontend/src/app/models/forms/updateuser.control.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { FormControl } from '@angular/forms';
|
||||
import Fuse from 'fuse.js';
|
||||
import { PermanentRolesList } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { ERole } from 'picsur-shared/dist/entities/role.entity';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import {
|
||||
CreatePasswordError,
|
||||
CreateUsernameError,
|
||||
PasswordValidators,
|
||||
UsernameValidators
|
||||
} from './default-validators';
|
||||
import { FullUserModel } from './fulluser.model';
|
||||
|
||||
export class UpdateUserControl {
|
||||
// Set once
|
||||
private fullRoles: ERole[] = [];
|
||||
private roles: string[] = [];
|
||||
|
||||
// Variables
|
||||
private selectableRolesSubject = new BehaviorSubject<string[]>([]);
|
||||
private rolesInputSubscription: null | Subscription;
|
||||
|
||||
public username = new FormControl('', UsernameValidators);
|
||||
public password = new FormControl('', PasswordValidators);
|
||||
|
||||
public rolesControl = new FormControl('', []);
|
||||
public selectableRoles = this.selectableRolesSubject.asObservable();
|
||||
public selectedRoles: string[] = [];
|
||||
|
||||
public get usernameValue() {
|
||||
return this.username.value;
|
||||
}
|
||||
|
||||
public get usernameError() {
|
||||
return CreateUsernameError(this.username.errors);
|
||||
}
|
||||
|
||||
public get passwordError() {
|
||||
return CreatePasswordError(this.password.errors);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.rolesInputSubscription = this.rolesControl.valueChanges.subscribe(
|
||||
(roles) => {
|
||||
this.updateSelectableRoles();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (this.rolesInputSubscription) {
|
||||
this.rolesInputSubscription.unsubscribe();
|
||||
this.rolesInputSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
public addRole(role: string) {
|
||||
if (!this.selectableRolesSubject.value.includes(role)) return;
|
||||
|
||||
this.selectedRoles.push(role);
|
||||
this.clearInput();
|
||||
}
|
||||
|
||||
public removeRole(role: string) {
|
||||
this.selectedRoles = this.selectedRoles.filter((r) => r !== role);
|
||||
this.updateSelectableRoles();
|
||||
}
|
||||
|
||||
public isRemovable(role: string) {
|
||||
if (PermanentRolesList.includes(role)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Data interaction
|
||||
|
||||
public putAllRoles(roles: ERole[]) {
|
||||
this.fullRoles = roles;
|
||||
this.roles = roles.map((role) => role.name);
|
||||
this.updateSelectableRoles();
|
||||
}
|
||||
|
||||
public putUsername(username: string) {
|
||||
this.username.setValue(username);
|
||||
}
|
||||
|
||||
public putRoles(roles: string[]) {
|
||||
this.selectedRoles = roles;
|
||||
this.updateSelectableRoles();
|
||||
}
|
||||
|
||||
public getData(): FullUserModel {
|
||||
return {
|
||||
username: this.username.value,
|
||||
password: this.password.value,
|
||||
roles: this.selectedRoles,
|
||||
};
|
||||
}
|
||||
|
||||
// Logic
|
||||
|
||||
private updateSelectableRoles() {
|
||||
const availableRoles = this.roles.filter(
|
||||
// Not available if either already selected, or the role is not addable/removable
|
||||
(r) => !(this.selectedRoles.includes(r) || PermanentRolesList.includes(r))
|
||||
);
|
||||
|
||||
const searchValue = this.rolesControl.value;
|
||||
if (searchValue && availableRoles.length > 0) {
|
||||
const fuse = new Fuse(availableRoles);
|
||||
const result = fuse
|
||||
.search(this.rolesControl.value ?? '')
|
||||
.map((r) => r.item);
|
||||
|
||||
this.selectableRolesSubject.next(result);
|
||||
} else {
|
||||
this.selectableRolesSubject.next(availableRoles);
|
||||
}
|
||||
}
|
||||
|
||||
private clearInput() {
|
||||
this.rolesControl.setValue('');
|
||||
}
|
||||
}
|
||||
@@ -1 +1,100 @@
|
||||
<p>settings-users-edit works!</p>
|
||||
<ng-container *ngIf="editing">
|
||||
<h1>Editing {{ model.usernameValue }}</h1>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="adding">
|
||||
<h1>Add new user</h1>
|
||||
</ng-container>
|
||||
|
||||
<form (ngSubmit)="updateUser()">
|
||||
<div class="row">
|
||||
<div class="col-12 py-2" *ngIf="updateFail">
|
||||
<mat-error *ngIf="adding"> Failed to add user </mat-error>
|
||||
<mat-error *ngIf="editing"> Failed to update user </mat-error>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" *ngIf="adding">
|
||||
<div class="col-lg-6 col-12">
|
||||
<mat-form-field appearance="outline" color="accent">
|
||||
<mat-label>Username</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[formControl]="model.username"
|
||||
name="username"
|
||||
required
|
||||
/>
|
||||
<mat-error *ngIf="model.username.errors">{{
|
||||
model.usernameError
|
||||
}}</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-12">
|
||||
<mat-form-field appearance="outline" color="accent">
|
||||
<mat-label>{{ editing ? "New Password" : "Password" }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="password"
|
||||
[formControl]="model.password"
|
||||
name="password"
|
||||
[required]="adding"
|
||||
/>
|
||||
<mat-error *ngIf="model.password.errors">{{
|
||||
model.passwordError
|
||||
}}</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-12">
|
||||
<mat-form-field appearance="outline" color="accent">
|
||||
<mat-label>Roles</mat-label>
|
||||
<mat-chip-list #chipList aria-label="Roles Selection">
|
||||
<mat-chip
|
||||
*ngFor="let role of model.selectedRoles"
|
||||
[removable]="model.isRemovable(role)"
|
||||
(removed)="removeRole(role)"
|
||||
>
|
||||
{{ role }}
|
||||
<button *ngIf="model.isRemovable(role)" matChipRemove>
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</mat-chip>
|
||||
<input
|
||||
placeholder="Add role..."
|
||||
#fruitInput
|
||||
[formControl]="model.rolesControl"
|
||||
[value]="model.rolesControl.value"
|
||||
[matAutocomplete]="auto"
|
||||
[matChipInputFor]="chipList"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
(matChipInputTokenEnd)="addRole($event)"
|
||||
/>
|
||||
</mat-chip-list>
|
||||
<mat-autocomplete
|
||||
#auto="matAutocomplete"
|
||||
(optionSelected)="selectedRole($event)"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let role of model.selectableRoles | async"
|
||||
[value]="role"
|
||||
>
|
||||
{{ role }}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 py-2">
|
||||
<button mat-raised-button color="accent" type="submit">
|
||||
{{ editing ? "Update" : "Add" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,128 @@
|
||||
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { UpdateUserControl } from 'src/app/models/forms/updateuser.control';
|
||||
import { SnackBarType } from 'src/app/models/snack-bar-type';
|
||||
import { RolesService } from 'src/app/services/api/roles.service';
|
||||
import { UserManageService } from 'src/app/services/api/usermanage.service';
|
||||
import { UtilService } from 'src/app/util/util.service';
|
||||
|
||||
enum EditMode {
|
||||
edit = 'edit',
|
||||
add = 'add',
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings-users-edit',
|
||||
templateUrl: './settings-users-edit.component.html',
|
||||
styleUrls: ['./settings-users-edit.component.scss']
|
||||
styleUrls: ['./settings-users-edit.component.scss'],
|
||||
})
|
||||
export class SettingsUsersEditComponent implements OnInit {
|
||||
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
|
||||
|
||||
constructor() { }
|
||||
private mode: EditMode = EditMode.edit;
|
||||
|
||||
ngOnInit(): void {
|
||||
model = new UpdateUserControl();
|
||||
updateFail: boolean = false;
|
||||
|
||||
get adding() {
|
||||
return this.mode === EditMode.add;
|
||||
}
|
||||
get editing() {
|
||||
return this.mode === EditMode.edit;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private userManageService: UserManageService,
|
||||
private utilService: UtilService,
|
||||
private rolesService: RolesService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
Promise.all([this.initUser(), this.initRoles()]).catch(console.error);
|
||||
}
|
||||
|
||||
private async initUser() {
|
||||
const username = this.route.snapshot.paramMap.get('username');
|
||||
if (!username) {
|
||||
this.mode = EditMode.add;
|
||||
return;
|
||||
}
|
||||
|
||||
this.mode = EditMode.edit;
|
||||
this.model.putUsername(username);
|
||||
|
||||
const user = await this.userManageService.getUser(username);
|
||||
if (HasFailed(user)) {
|
||||
this.utilService.showSnackBar('Failed to get user', SnackBarType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.putUsername(user.username);
|
||||
this.model.putRoles(user.roles);
|
||||
}
|
||||
|
||||
private async initRoles() {
|
||||
const roles = await this.rolesService.getRoles();
|
||||
if (HasFailed(roles)) {
|
||||
this.utilService.showSnackBar('Failed to get roles', SnackBarType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.putAllRoles(roles);
|
||||
}
|
||||
|
||||
removeRole(role: string) {
|
||||
this.model.removeRole(role);
|
||||
}
|
||||
|
||||
addRole(event: MatChipInputEvent) {
|
||||
const value = (event.value ?? '').trim();
|
||||
this.model.addRole(value);
|
||||
}
|
||||
|
||||
selectedRole(event: MatAutocompleteSelectedEvent): void {
|
||||
this.model.addRole(event.option.viewValue);
|
||||
}
|
||||
|
||||
async updateUser() {
|
||||
const data = this.model.getData();
|
||||
|
||||
if (this.adding) {
|
||||
const resultUser = await this.userManageService.createUser(data);
|
||||
if (HasFailed(resultUser)) {
|
||||
this.utilService.showSnackBar(
|
||||
'Failed to create user',
|
||||
SnackBarType.Error
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.utilService.showSnackBar('User created', SnackBarType.Success);
|
||||
} else {
|
||||
const updateData = data.password
|
||||
? data
|
||||
: { username: data.username, roles: data.roles };
|
||||
|
||||
const resultUser = await this.userManageService.updateUser(
|
||||
updateData as any
|
||||
);
|
||||
if (HasFailed(resultUser)) {
|
||||
this.utilService.showSnackBar(
|
||||
'Failed to update user',
|
||||
SnackBarType.Error
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.utilService.showSnackBar('User updated', SnackBarType.Success);
|
||||
}
|
||||
|
||||
this.router.navigate(['/settings/users']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<h1>Users</h1>
|
||||
|
||||
<mat-table [dataSource]="dataSubject" class="mat-elevation-z2">
|
||||
<ng-container matColumnDef="id">
|
||||
<!-- <ng-container matColumnDef="id">
|
||||
<mat-header-cell *matHeaderCellDef>ID</mat-header-cell>
|
||||
<mat-cell *matCellDef="let user">{{ user.id }}</mat-cell>
|
||||
</ng-container>
|
||||
</ng-container> -->
|
||||
|
||||
<ng-container matColumnDef="username">
|
||||
<mat-header-cell *matHeaderCellDef>Username</mat-header-cell>
|
||||
@@ -14,7 +14,7 @@
|
||||
<ng-container matColumnDef="actions">
|
||||
<mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
|
||||
<mat-cell *matCellDef="let user">
|
||||
<button mat-icon-button>
|
||||
<button mat-icon-button (click)="editUser(user)">
|
||||
<mat-icon aria-label="Edit">edit</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
@@ -24,8 +24,23 @@
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"> pog </mat-row>
|
||||
</mat-table>
|
||||
|
||||
<mat-paginator
|
||||
color="accent"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
[pageSize]="startingPageSize"
|
||||
length="Infinity"
|
||||
aria-label="Select page of users"
|
||||
(page)="updateSubject.next($event)"
|
||||
>
|
||||
</mat-paginator>
|
||||
|
||||
<div class="fabholder">
|
||||
<button mat-fab color="accent" class="fabbutton fullanimate mat-elevation-z6">
|
||||
<button
|
||||
mat-fab
|
||||
color="accent"
|
||||
class="fabbutton fullanimate mat-elevation-z6"
|
||||
(click)="addUser()"
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { Router } from '@angular/router';
|
||||
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
|
||||
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
@@ -13,7 +14,7 @@ import { UtilService } from 'src/app/util/util.service';
|
||||
styleUrls: ['./settings-users.component.scss'],
|
||||
})
|
||||
export class SettingsUsersComponent implements OnInit {
|
||||
public readonly displayedColumns: string[] = ['id', 'username', 'actions'];
|
||||
public readonly displayedColumns: string[] = [/*'id',*/ 'username', 'actions'];
|
||||
public readonly pageSizeOptions: number[] = [5, 10, 25, 100];
|
||||
public readonly startingPageSize = this.pageSizeOptions[2];
|
||||
|
||||
@@ -24,7 +25,8 @@ export class SettingsUsersComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private userManageService: UserManageService,
|
||||
private utilService: UtilService
|
||||
private utilService: UtilService,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -32,6 +34,14 @@ export class SettingsUsersComponent implements OnInit {
|
||||
this.fetchUsers(this.startingPageSize, 0);
|
||||
}
|
||||
|
||||
public editUser(user: EUser) {
|
||||
this.router.navigate(['/settings/users/edit', user.username]);
|
||||
}
|
||||
|
||||
public addUser() {
|
||||
this.router.navigate(['/settings/users/add']);
|
||||
}
|
||||
|
||||
@AutoUnsubscribe()
|
||||
private subscribeToUpdate() {
|
||||
return this.updateSubject
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { SettingsUsersEditComponent } from './settings-users-edit/settings-users-edit.component';
|
||||
@@ -19,6 +23,11 @@ import { SettingsUsersRoutingModule } from './settings-users.routing.module';
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatChipsModule,
|
||||
MatAutocompleteModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
})
|
||||
export class SettingsUsersRouteModule {}
|
||||
|
||||
@@ -10,7 +10,11 @@ const routes: PRoutes = [
|
||||
component: SettingsUsersComponent,
|
||||
},
|
||||
{
|
||||
path: 'edit/:id',
|
||||
path: 'edit/:username',
|
||||
component: SettingsUsersEditComponent,
|
||||
},
|
||||
{
|
||||
path: 'add',
|
||||
component: SettingsUsersEditComponent,
|
||||
}
|
||||
];
|
||||
|
||||
4
frontend/src/app/routes/user/login/login.component.scss
Normal file
4
frontend/src/app/routes/user/login/login.component.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
mat-form-field {
|
||||
max-width: 40rem;
|
||||
width: inherit;
|
||||
}
|
||||
@@ -7,11 +7,12 @@ import { SnackBarType } from 'src/app/models/snack-bar-type';
|
||||
import { PermissionService } from 'src/app/services/api/permission.service';
|
||||
import { UserService } from 'src/app/services/api/user.service';
|
||||
import { UtilService } from 'src/app/util/util.service';
|
||||
import { LoginControl } from '../../../models/forms/login.model';
|
||||
import { UserPassModel } from '../../../models/forms/userpass';
|
||||
import { LoginControl } from '../../../models/forms/login.control';
|
||||
import { UserPassModel } from '../../../models/forms/userpass.model';
|
||||
|
||||
@Component({
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss'],
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
private readonly logger = console;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
mat-form-field {
|
||||
max-width: 40rem;
|
||||
width: inherit;
|
||||
}
|
||||
@@ -3,15 +3,16 @@ import { Router } from '@angular/router';
|
||||
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
|
||||
import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { UserPassModel } from 'src/app/models/forms/userpass';
|
||||
import { UserPassModel } from 'src/app/models/forms/userpass.model';
|
||||
import { SnackBarType } from 'src/app/models/snack-bar-type';
|
||||
import { PermissionService } from 'src/app/services/api/permission.service';
|
||||
import { UserService } from 'src/app/services/api/user.service';
|
||||
import { UtilService } from 'src/app/util/util.service';
|
||||
import { RegisterControl } from '../../../models/forms/register.model';
|
||||
import { RegisterControl } from '../../../models/forms/register.control';
|
||||
|
||||
@Component({
|
||||
templateUrl: './register.component.html',
|
||||
styleUrls: ['./register.component.scss'],
|
||||
})
|
||||
export class RegisterComponent implements OnInit {
|
||||
private readonly logger = console;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import Fuse from 'fuse.js';
|
||||
import { CopyFieldModule } from 'src/app/components/copyfield/copyfield.module';
|
||||
import { ViewComponent } from './view.component';
|
||||
import { ViewRoutingModule } from './view.routing.module';
|
||||
|
||||
const a = Fuse;
|
||||
@NgModule({
|
||||
declarations: [ViewComponent],
|
||||
imports: [CommonModule, CopyFieldModule, ViewRoutingModule, MatButtonModule],
|
||||
|
||||
25
frontend/src/app/services/api/roles.service.ts
Normal file
25
frontend/src/app/services/api/roles.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { RoleListResponse } from 'picsur-shared/dist/dto/api/roles.dto';
|
||||
import { ERole } from 'picsur-shared/dist/entities/role.entity';
|
||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { ApiService } from './api.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RolesService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
public async getRoles(): AsyncFailable<ERole[]> {
|
||||
const result = await this.apiService.get(
|
||||
RoleListResponse,
|
||||
'/api/roles/list'
|
||||
);
|
||||
|
||||
if (HasFailed(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return result.roles;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
UserCreateRequest,
|
||||
UserCreateResponse,
|
||||
UserInfoRequest,
|
||||
UserInfoResponse,
|
||||
UserListRequest,
|
||||
UserListResponse
|
||||
UserListResponse,
|
||||
UserUpdateRequest,
|
||||
UserUpdateResponse
|
||||
} from 'picsur-shared/dist/dto/api/usermanage.dto';
|
||||
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { FullUserModel } from 'src/app/models/forms/fulluser.model';
|
||||
import { ApiService } from './api.service';
|
||||
|
||||
@Injectable({
|
||||
@@ -13,6 +20,21 @@ import { ApiService } from './api.service';
|
||||
export class UserManageService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
public async getUser(username: string): AsyncFailable<EUser> {
|
||||
const body = {
|
||||
username,
|
||||
};
|
||||
|
||||
const result = await this.apiService.post(
|
||||
UserInfoRequest,
|
||||
UserInfoResponse,
|
||||
'api/user/info',
|
||||
body
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getUsers(count: number, page: number): AsyncFailable<EUser[]> {
|
||||
const body = {
|
||||
count,
|
||||
@@ -32,4 +54,26 @@ export class UserManageService {
|
||||
|
||||
return result.users;
|
||||
}
|
||||
|
||||
public async createUser(user: FullUserModel): AsyncFailable<EUser> {
|
||||
const result = await this.apiService.post(
|
||||
UserCreateRequest,
|
||||
UserCreateResponse,
|
||||
'/api/user/create',
|
||||
user
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async updateUser(user: FullUserModel): AsyncFailable<EUser> {
|
||||
const result = await this.apiService.post(
|
||||
UserUpdateRequest,
|
||||
UserUpdateResponse,
|
||||
'/api/user/update',
|
||||
user
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,3 @@ html {
|
||||
width: initial !important;
|
||||
}
|
||||
|
||||
// Fix small form inputs
|
||||
|
||||
form mat-form-field {
|
||||
width: inherit;
|
||||
max-width: 40rem;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
border-style: solid;
|
||||
border-width: 5px;
|
||||
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
// Easily center content
|
||||
@@ -63,6 +65,11 @@
|
||||
|
||||
// Anim
|
||||
|
||||
.fullanimate, .fullanimate * {
|
||||
.container, .row > div {
|
||||
transition: ease-in-out all 0.2s;
|
||||
}
|
||||
|
||||
.fullanimate,
|
||||
.fullanimate * {
|
||||
transition: ease-in-out all 0.2s !important;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ import {
|
||||
IsArray,
|
||||
IsDefined,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
ValidateNested
|
||||
} from 'class-validator';
|
||||
import { EUser, NamePassUser, UsernameUser } from '../../entities/user.entity';
|
||||
import { IsPlainTextPwd } from '../../validators/user.validators';
|
||||
import { Roles } from '../roles.dto';
|
||||
|
||||
// UserList
|
||||
@@ -42,7 +44,12 @@ export class UserListResponse {
|
||||
}
|
||||
|
||||
// UserCreate
|
||||
export class UserCreateRequest extends NamePassUser {}
|
||||
export class UserCreateRequest extends NamePassUser {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
roles?: Roles;
|
||||
}
|
||||
export class UserCreateResponse extends EUser {}
|
||||
|
||||
// UserDelete
|
||||
@@ -54,11 +61,15 @@ export class UserInfoRequest extends UsernameUser {}
|
||||
export class UserInfoResponse extends EUser {}
|
||||
|
||||
// UserUpdateRoles
|
||||
export class UserUpdateRolesRequest extends UsernameUser {
|
||||
export class UserUpdateRequest extends UsernameUser {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsDefined()
|
||||
@IsString({ each: true })
|
||||
roles: Roles;
|
||||
roles?: Roles;
|
||||
|
||||
@IsPlainTextPwd()
|
||||
@IsOptional()
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export class UserUpdateRolesResponse extends EUser {}
|
||||
export class UserUpdateResponse extends EUser {}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { Permission, Permissions, PermissionsList } from './permissions';
|
||||
|
||||
// Config
|
||||
|
||||
// These roles can never be removed from a user
|
||||
// These roles can never be removed or added to a user.
|
||||
const PermanentRolesTuple = tuple('guest', 'user');
|
||||
// These reles can never be modified
|
||||
// These roles can never be modified
|
||||
const ImmuteableRolesTuple = tuple('admin');
|
||||
// These roles can never be removed from the server
|
||||
const SystemRolesTuple = tuple(...PermanentRolesTuple, ...ImmuteableRolesTuple);
|
||||
|
||||
17
yarn.lock
17
yarn.lock
@@ -4252,6 +4252,11 @@ functional-red-black-tree@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
||||
|
||||
fuse.js@^6.5.3:
|
||||
version "6.5.3"
|
||||
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.5.3.tgz#7446c0acbc4ab0ab36fa602e97499bdb69452b93"
|
||||
integrity sha512-sA5etGE7yD/pOqivZRBvUBd/NaL2sjAu6QuSaFoe1H2BrJSkH/T/UXAJ8CdXdw7DvY3Hs8CXKYkDWX7RiP5KOg==
|
||||
|
||||
gauge@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395"
|
||||
@@ -5494,7 +5499,17 @@ minimatch@^3.0.4:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@1.2.5, minimist@^1.2.0, minimist@^1.2.6, "minimist@npm:minimist-lite":
|
||||
minimist@1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.6:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
"minimist@npm:minimist-lite":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/minimist-lite/-/minimist-lite-2.2.1.tgz#abb71db2c9b454d7cf4496868c03e9802de9934d"
|
||||
integrity sha512-RSrWIRWGYoM2TDe102s7aIyeSipXMIXKb1fSHYx1tAbxAV0z4g2xR6ra3oPzkTqFb0EIUz1H3A/qvYYeDd+/qQ==
|
||||
|
||||
Reference in New Issue
Block a user