relocate special roles to api request

This commit is contained in:
rubikscraft
2022-03-24 19:56:26 +01:00
parent 25b85c00e0
commit 95c8f630f1
33 changed files with 284 additions and 156 deletions

View File

@@ -12,10 +12,11 @@
"prebuild": "rimraf dist",
"build": "nest build",
"start": "nest start --exec \"node --experimental-specifier-resolution=node\"",
"start:dev": "nest start --watch --exec \"node --experimental-specifier-resolution=node\"",
"start:dev": "yarn clean && nest start --watch --exec \"node --experimental-specifier-resolution=node\"",
"start:debug": "nest start --debug --watch --exec \"node --experimental-specifier-resolution=node\"",
"start:prod": "node --experimental-specifier-resolution=node dist/main",
"format": "prettier --write \"src/**/*.ts\"",
"clean": "rimraf dist",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
},
"dependencies": {

View File

@@ -1,14 +1,9 @@
import { Logger, Module, OnModuleInit } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
ImmuteableRolesList,
SystemRoleDefaults,
SystemRoles,
SystemRolesList
} from 'picsur-shared/dist/dto/roles.dto';
import { HasFailed } from 'picsur-shared/dist/types';
import { PicsurConfigModule } from '../../config/config.module';
import { HostConfigService } from '../../config/host.config.service';
import { ImmutableRolesList, SystemRoleDefaults, UndeletableRolesList } from '../../models/dto/roles.dto';
import { ERoleBackend } from '../../models/entities/role.entity';
import { RolesService } from './roledb.service';
@@ -43,7 +38,7 @@ export class RolesModule implements OnModuleInit {
}
private async ensureSystemRolesExist() {
for (const systemRole of SystemRolesList as SystemRoles) {
for (const systemRole of UndeletableRolesList) {
this.logger.debug(`Ensuring system role "${systemRole}" exists`);
const exists = await this.rolesService.exists(systemRole);
@@ -63,7 +58,7 @@ export class RolesModule implements OnModuleInit {
}
private async updateImmutableRoles() {
for (const immutableRole of ImmuteableRolesList as SystemRoles) {
for (const immutableRole of ImmutableRolesList) {
this.logger.debug(
`Updating permissions for immutable role "${immutableRole}"`,
);

View File

@@ -2,11 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { plainToClass } from 'class-transformer';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import {
ImmuteableRolesList,
Roles,
SystemRolesList
} from 'picsur-shared/dist/dto/roles.dto';
import {
AsyncFailable,
Fail,
@@ -15,6 +10,7 @@ import {
} from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { In, Repository } from 'typeorm';
import { ImmutableRolesList, UndeletableRolesList } from '../../models/dto/roles.dto';
import { ERoleBackend } from '../../models/entities/role.entity';
@Injectable()
@@ -51,7 +47,7 @@ export class RolesService {
const roleToModify = await this.resolve(role);
if (HasFailed(roleToModify)) return roleToModify;
if (SystemRolesList.includes(roleToModify.name)) {
if (UndeletableRolesList.includes(roleToModify.name)) {
return Fail('Cannot delete system role');
}
@@ -62,7 +58,7 @@ export class RolesService {
}
}
public async getPermissions(roles: Roles): AsyncFailable<Permissions> {
public async getPermissions(roles: string[]): AsyncFailable<Permissions> {
const permissions: Permissions = [];
const foundRoles = await Promise.all(
roles.map((role: string) => this.findOne(role)),
@@ -113,7 +109,7 @@ export class RolesService {
const roleToModify = await this.resolve(role);
if (HasFailed(roleToModify)) return roleToModify;
if (!allowImmutable && ImmuteableRolesList.includes(roleToModify.name)) {
if (!allowImmutable && ImmutableRolesList.includes(roleToModify.name)) {
return Fail('Cannot modify immutable role');
}
@@ -157,7 +153,7 @@ export class RolesService {
if (!iamsure) return Fail('Nuke aborted');
try {
await this.rolesRepository.delete({
name: In(SystemRolesList),
name: In(UndeletableRolesList),
});
} catch (e: any) {
return Fail(e?.message);

View File

@@ -2,11 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcrypt';
import { plainToClass } from 'class-transformer';
import {
DefaultRolesList,
PermanentRolesList,
Roles
} from 'picsur-shared/dist/dto/roles.dto';
import {
LockedLoginUsersList,
LockedPermsUsersList,
@@ -20,6 +15,10 @@ import {
} from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { Repository } from 'typeorm';
import {
DefaultRolesList,
SoulBoundRolesList
} from '../../models/dto/roles.dto';
import { EUserBackend } from '../../models/entities/user.entity';
import { GetCols } from '../collectionutils';
import { RolesService } from '../roledb/roledb.service';
@@ -41,7 +40,7 @@ export class UsersService {
public async create(
username: string,
password: string,
roles?: Roles,
roles?: string[],
byPassRoleCheck?: boolean,
): AsyncFailable<EUserBackend> {
if (await this.exists(username)) return Fail('User already exists');
@@ -90,7 +89,7 @@ export class UsersService {
public async setRoles(
user: string | EUserBackend,
roles: Roles,
roles: string[],
): AsyncFailable<EUserBackend> {
const userToModify = await this.resolve(user);
if (HasFailed(userToModify)) return userToModify;
@@ -101,7 +100,7 @@ export class UsersService {
}
const rolesToKeep = userToModify.roles.filter((role) =>
PermanentRolesList.includes(role),
SoulBoundRolesList.includes(role),
);
const rolesToAdd = this.filterAddedRoles(roles);
@@ -216,9 +215,9 @@ export class UsersService {
}
}
private filterAddedRoles(roles: Roles): Roles {
private filterAddedRoles(roles: string[]): string[] {
const filteredRoles = roles.filter(
(role) => !PermanentRolesList.includes(role),
(role) => !SoulBoundRolesList.includes(role),
);
return filteredRoles;

View File

@@ -1,6 +1,5 @@
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';
@@ -23,7 +22,7 @@ export class UserRolesService {
public async addRoles(
user: string | EUserBackend,
roles: Roles,
roles: string[],
): AsyncFailable<EUserBackend> {
const userToModify = await this.usersService.resolve(user);
if (HasFailed(userToModify)) return userToModify;
@@ -35,7 +34,7 @@ export class UserRolesService {
public async removeRoles(
user: string | EUserBackend,
roles: Roles,
roles: string[],
): AsyncFailable<EUserBackend> {
const userToModify = await this.usersService.resolve(user);
if (HasFailed(userToModify)) return userToModify;

View File

@@ -1,9 +1,9 @@
import {
BadRequestException,
Injectable,
Logger,
PipeTransform,
Scope
BadRequestException,
Injectable,
Logger,
PipeTransform,
Scope
} from '@nestjs/common';
import { FastifyRequest } from 'fastify';
import { MultipartFields, MultipartFile } from 'fastify-multipart';
@@ -11,9 +11,9 @@ import { Newable } from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { MultipartConfigService } from '../config/multipart.config.service';
import {
MultiPartFieldDto,
MultiPartFileDto
} from '../models/dto/multipart.dto';
MultiPartFieldDto,
MultiPartFileDto
} from '../models/requests/multipart.dto';
@Injectable({ scope: Scope.REQUEST })
export class MultiPartPipe implements PipeTransform {

View File

@@ -2,7 +2,7 @@ import {
ArgumentsHost, Catch, ExceptionFilter, HttpException
} from '@nestjs/common';
import { FastifyReply, FastifyRequest } from 'fastify';
import { ApiErrorResponse } from 'picsur-shared/dist/dto/api';
import { ApiErrorResponse } from 'picsur-shared/dist/dto/api/api.dto';
@Catch(HttpException)
export class MainExceptionFilter implements ExceptionFilter {

View File

@@ -2,7 +2,7 @@ import {
CallHandler, ExecutionContext, Injectable,
NestInterceptor
} from '@nestjs/common';
import { ApiResponse } from 'picsur-shared/dist/dto/api';
import { ApiResponse } from 'picsur-shared/dist/dto/api/api.dto';
import { map, Observable } from 'rxjs';
@Injectable()

View File

@@ -0,0 +1,39 @@
import { Permission, Permissions, PermissionsList } from 'picsur-shared/dist/dto/permissions';
import tuple from 'picsur-shared/dist/types/tuple';
// Config
// These roles can never be removed or added to a user.
const SoulBoundRolesTuple = tuple('guest', 'user');
// These roles can never be modified
const ImmutableRolesTuple = tuple('admin');
// These roles can never be removed from the server
const UndeletableRolesTuple = tuple(...SoulBoundRolesTuple, ...ImmutableRolesTuple);
// These roles will be applied by default to new users
export const DefaultRolesList: string[] = ['user'];
// Derivatives
export const SoulBoundRolesList: string[] = SoulBoundRolesTuple;
export const ImmutableRolesList: string[] = ImmutableRolesTuple;
export const UndeletableRolesList: string[] = UndeletableRolesTuple;
// Defaults
type SystemRole = typeof UndeletableRolesTuple[number];
const SystemRoleDefaultsTyped: {
[key in SystemRole]: Permissions;
} = {
guest: [Permission.ImageView, Permission.UserLogin],
user: [
Permission.ImageView,
Permission.UserMe,
Permission.UserLogin,
Permission.Settings,
Permission.ImageUpload,
],
// Grant all permissions to admin
admin: PermissionsList,
};
export const SystemRoleDefaults = SystemRoleDefaultsTyped as {
[key in string]: Permissions;
};

View File

@@ -1,4 +1,3 @@
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
import { EUser } from 'picsur-shared/dist/entities/user.entity';
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
@@ -14,7 +13,7 @@ export class EUserBackend extends EUser {
override username: string;
@Column('text', { nullable: false, array: true })
override roles: Roles;
override roles: string[];
@Column({ nullable: false, select: false })
override password?: string;

View File

@@ -1,5 +1,5 @@
import { Controller, Get, Request } from '@nestjs/common';
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
import AuthFasityRequest from '../../../models/requests/authrequest.dto';
@Controller('api/experiment')

View File

@@ -6,6 +6,7 @@ import {
Logger,
Post
} from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import {
RoleCreateRequest,
RoleCreateResponse,
@@ -15,12 +16,19 @@ import {
RoleInfoResponse,
RoleListResponse,
RoleUpdateRequest,
RoleUpdateResponse
RoleUpdateResponse,
SpecialRolesResponse
} from 'picsur-shared/dist/dto/api/roles.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { HasFailed } from 'picsur-shared/dist/types';
import { RolesService } from '../../../collections/roledb/roledb.service';
import { RequiredPermissions } from '../../../decorators/permissions.decorator';
import {
DefaultRolesList,
ImmutableRolesList,
SoulBoundRolesList,
UndeletableRolesList
} from '../../../models/dto/roles.dto';
@Controller('api/roles')
@RequiredPermissions(Permission.RoleManage)
@@ -95,4 +103,16 @@ export class RolesController {
return deletedRole;
}
@Get('special')
async getSpecialRoles(): Promise<SpecialRolesResponse> {
const result: SpecialRolesResponse = {
SoulBoundRoles: SoulBoundRolesList,
ImmutableRoles: ImmutableRolesList,
UndeletableRoles: UndeletableRolesList,
DefaultRoles: DefaultRolesList,
};
return plainToClass(SpecialRolesResponse, result);
}
}

View File

@@ -1,30 +1,30 @@
import {
Body,
Controller,
Get,
InternalServerErrorException,
Logger,
Post,
Request
Body,
Controller,
Get,
InternalServerErrorException,
Logger,
Post,
Request
} from '@nestjs/common';
import {
UserLoginResponse,
UserMePermissionsResponse,
UserMeResponse,
UserRegisterRequest,
UserRegisterResponse
UserLoginResponse,
UserMePermissionsResponse,
UserMeResponse,
UserRegisterRequest,
UserRegisterResponse
} from 'picsur-shared/dist/dto/api/user.dto';
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,
UseLocalAuth
NoPermissions,
RequiredPermissions,
UseLocalAuth
} from '../../../decorators/permissions.decorator';
import { AuthManagerService } from '../../../managers/auth/auth.service';
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
import AuthFasityRequest from '../../../models/requests/authrequest.dto';
@Controller('api/user')
export class UserController {

View File

@@ -18,7 +18,7 @@ import { HasFailed } from 'picsur-shared/dist/types';
import { MultiPart } from '../../decorators/multipart.decorator';
import { RequiredPermissions } from '../../decorators/permissions.decorator';
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
import { ImageUploadDto } from '../../models/dto/imageroute.dto';
import { ImageUploadDto } from '../../models/requests/imageroute.dto';
@Controller('i')
@RequiredPermissions(Permission.ImageView)

View File

@@ -1,7 +1,5 @@
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
export interface FullUserModel {
username: string;
password: string;
roles: Roles;
roles: string[];
}

View File

@@ -1,7 +1,6 @@
import { FormControl } from '@angular/forms';
import Fuse from 'fuse.js';
import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions';
import { PermanentRolesList } from 'picsur-shared/dist/dto/roles.dto';
import { BehaviorSubject, Subscription } from 'rxjs';
import { RoleNameValidators } from './role-validators';
import { RoleModel } from './role.model';
@@ -58,11 +57,6 @@ export class UpdateRoleControl {
this.updateSelectablePermissions();
}
public isRemovable(role: Permission) {
if (PermanentRolesList.includes(role)) return false;
return true;
}
// Data interaction
public putAllPermissions(permissions: Permissions) {

View File

@@ -1,7 +1,6 @@
import { FormControl } from '@angular/forms';
import Fuse from 'fuse.js';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import { PermanentRolesList } from 'picsur-shared/dist/dto/roles.dto';
import { ERole } from 'picsur-shared/dist/entities/role.entity';
import { BehaviorSubject, Subscription } from 'rxjs';
import { FullUserModel } from './fulluser.model';
@@ -13,6 +12,9 @@ import {
} from './user-validators';
export class UpdateUserControl {
// Special roles
private SoulBoundRolesList: string[] = [];
// Set once
private fullRoles: ERole[] = [];
private roles: string[] = [];
@@ -68,7 +70,7 @@ export class UpdateUserControl {
}
public isRemovable(role: string) {
if (PermanentRolesList.includes(role)) return false;
if (this.SoulBoundRolesList.includes(role)) return false;
return true;
}
@@ -106,6 +108,10 @@ export class UpdateUserControl {
this.updateSelectableRoles();
}
public putSoulBoundRoles(roles: string[]) {
this.SoulBoundRolesList = roles;
}
public getData(): FullUserModel {
return {
username: this.username.value,
@@ -119,7 +125,8 @@ export class UpdateUserControl {
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))
(r) =>
!(this.selectedRoles.includes(r) || this.SoulBoundRolesList.includes(r))
);
const searchValue = this.rolesControl.value;

View File

@@ -33,12 +33,10 @@
<mat-chip-list #chipList aria-label="Permissions Selection">
<mat-chip
*ngFor="let permission of model.selectedPermissions"
[removable]="model.isRemovable(permission)"
[disabled]="!model.isRemovable(permission)"
(removed)="removePermission(permission)"
>
{{ uiFriendlyPermission(permission) }}
<button *ngIf="model.isRemovable(permission)" matChipRemove>
<button matChipRemove>
<mat-icon>cancel</mat-icon>
</button>
</mat-chip>
@@ -50,14 +48,14 @@
[matAutocomplete]="auto"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addRole($event)"
(matChipInputTokenEnd)="addPermission($event)"
autocorrect="off"
autocapitalize="none"
/>
</mat-chip-list>
<mat-autocomplete
#auto="matAutocomplete"
(optionSelected)="selectedRole($event)"
(optionSelected)="selectedPermission($event)"
>
<mat-option
*ngFor="let permission of model.selectablePermissions | async"

View File

@@ -77,12 +77,12 @@ export class SettingsRolesEditComponent implements OnInit {
this.model.removePermission(permission);
}
addRole(event: MatChipInputEvent) {
addPermission(event: MatChipInputEvent) {
const value = (event.value ?? '').trim();
this.model.addPermission(value as Permission);
}
selectedRole(event: MatAutocompleteSelectedEvent): void {
selectedPermission(event: MatAutocompleteSelectedEvent): void {
this.model.addPermission(event.option.viewValue as Permission);
}

View File

@@ -6,7 +6,6 @@ import {
Permission,
UIFriendlyPermissions
} from 'picsur-shared/dist/dto/permissions';
import { ImmuteableRolesList, SystemRolesList } from 'picsur-shared/dist/dto/roles.dto';
import { ERole } from 'picsur-shared/dist/entities/role.entity';
import { HasFailed } from 'picsur-shared/dist/types';
import { SnackBarType } from 'src/app/models/snack-bar-type';
@@ -29,6 +28,9 @@ export class SettingsRolesComponent implements OnInit, AfterViewInit {
public dataSource = new MatTableDataSource<ERole>([]);
private UndeletableRolesList: string[] = [];
private ImmutableRolesList: string[] = [];
@ViewChild(MatPaginator) paginator: MatPaginator;
constructor(
@@ -91,11 +93,11 @@ export class SettingsRolesComponent implements OnInit, AfterViewInit {
}
isSystem(role: ERole) {
return SystemRolesList.includes(role.name);
return this.UndeletableRolesList.includes(role.name);
}
isImmutable(role: ERole) {
return ImmuteableRolesList.includes(role.name);
return this.ImmutableRolesList.includes(role.name);
}
private async fetchRoles() {
@@ -106,5 +108,17 @@ export class SettingsRolesComponent implements OnInit, AfterViewInit {
}
this.dataSource.data = roles;
const specialRoles = await this.rolesService.getSpecialRoles();
if (HasFailed(specialRoles)) {
this.utilService.showSnackBar(
'Failed to load special roles',
SnackBarType.Error
);
return;
}
this.UndeletableRolesList = specialRoles.UndeletableRoles;
this.ImmutableRolesList = specialRoles.ImmutableRoles;
}
}

View File

@@ -4,7 +4,6 @@ import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute, Router } from '@angular/router';
import { UIFriendlyPermissions } from 'picsur-shared/dist/dto/permissions';
import { DefaultRolesList } from 'picsur-shared/dist/dto/roles.dto';
import { LockedPermsUsersList } from 'picsur-shared/dist/dto/specialusers.dto';
import { HasFailed } from 'picsur-shared/dist/types';
import { UpdateUserControl } from 'src/app/models/forms/updateuser.control';
@@ -51,9 +50,15 @@ export class SettingsUsersEditComponent implements OnInit {
private async initUser() {
const username = this.route.snapshot.paramMap.get('username');
const { DefaultRoles, SoulBoundRoles } =
await this.rolesService.getSpecialRolesOptimistic();
this.model.putSoulBoundRoles(SoulBoundRoles);
if (!username) {
this.mode = EditMode.add;
this.model.putRoles(DefaultRolesList);
this.model.putRoles(DefaultRoles);
return;
}

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { ClassConstructor, plainToClass } from 'class-transformer';
import { ApiResponse, ApiSuccessResponse } from 'picsur-shared/dist/dto/api';
import { ApiResponse, ApiSuccessResponse } from 'picsur-shared/dist/dto/api/api.dto';
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { Subject } from 'rxjs';

View File

@@ -0,0 +1,44 @@
import { Injectable } from '@angular/core';
interface dataWrapper<T> {
data: T;
expires: number;
}
@Injectable({
providedIn: 'root',
})
export class CacheService {
private readonly cacheExpiresMS = 1000 * 60 * 60;
private storage: Storage;
constructor() {
if (window.sessionStorage) {
this.storage = window.sessionStorage;
} else {
throw new Error('Session storage is not supported');
}
}
public get<T>(key: string): T | null {
try {
const data: dataWrapper<T> = JSON.parse(this.storage.getItem(key) ?? '');
if (data && data.data && data.expires > Date.now()) {
return data.data;
}
return null;
} catch (e) {
return null;
}
}
public set<T>(key: string, value: T): void {
const data: dataWrapper<T> = {
data: value,
expires: Date.now() + this.cacheExpiresMS,
};
this.storage.setItem(key, JSON.stringify(data));
}
}

View File

@@ -8,18 +8,23 @@ import {
RoleInfoResponse,
RoleListResponse,
RoleUpdateRequest,
RoleUpdateResponse
RoleUpdateResponse,
SpecialRolesResponse
} 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 { RoleModel } from 'src/app/models/forms/role.model';
import { ApiService } from './api.service';
import { CacheService } from './cache.service';
@Injectable({
providedIn: 'root',
})
export class RolesService {
constructor(private apiService: ApiService) {}
constructor(
private apiService: ApiService,
private cacheService: CacheService
) {}
public async getRoles(): AsyncFailable<ERole[]> {
const result = await this.apiService.get(
@@ -85,4 +90,38 @@ export class RolesService {
return result;
}
public async getSpecialRoles(): AsyncFailable<SpecialRolesResponse> {
const cached = this.cacheService.get<SpecialRolesResponse>('specialRoles');
if (cached !== null) {
return cached;
}
const result = await this.apiService.get(
SpecialRolesResponse,
'/api/roles/special'
);
if (HasFailed(result)) {
return result;
}
this.cacheService.set('specialRoles', result);
return result;
}
public async getSpecialRolesOptimistic(): Promise<SpecialRolesResponse> {
const result = await this.getSpecialRoles();
if (HasFailed(result)) {
return {
DefaultRoles: [],
ImmutableRoles: [],
SoulBoundRoles: [],
UndeletableRoles: [],
};
}
return result;
}
}

View File

@@ -18,7 +18,8 @@
"typescript": "4.5.5"
},
"scripts": {
"start": "tsc-watch",
"build": "tsc"
"clean": "rm -rf ./dist",
"start": "yarn clean && tsc-watch",
"build": "yarn clean && tsc"
}
}

View File

@@ -1,14 +1,12 @@
import { Type } from 'class-transformer';
import {
IsArray,
IsDefined, ValidateNested
} from 'class-validator';
import { IsArray, IsDefined, ValidateNested } from 'class-validator';
import {
ERole,
RoleNameObject,
RoleNamePermsObject
} from '../../entities/role.entity';
import { IsPosInt } from '../../validators/positive-int.validator';
import { IsStringList } from '../../validators/string-list.validator';
// RoleInfo
export class RoleInfoRequest extends RoleNameObject {}
@@ -37,3 +35,22 @@ export class RoleCreateResponse extends ERole {}
// RoleDelete
export class RoleDeleteRequest extends RoleNameObject {}
export class RoleDeleteResponse extends ERole {}
// SpecialRoles
export class SpecialRolesResponse {
@IsDefined()
@IsStringList()
SoulBoundRoles: string[];
@IsDefined()
@IsStringList()
ImmutableRoles: string[];
@IsDefined()
@IsStringList()
UndeletableRoles: string[];
@IsDefined()
@IsStringList()
DefaultRoles: string[];
}

View File

@@ -1,13 +1,13 @@
import { Type } from 'class-transformer';
import {
IsArray,
IsDefined, IsOptional,
IsString, ValidateNested
IsDefined,
IsOptional, ValidateNested
} from 'class-validator';
import { EUser, NamePassUser, UsernameUser } from '../../entities/user.entity';
import { IsPosInt } from '../../validators/positive-int.validator';
import { IsStringList } from '../../validators/string-list.validator';
import { IsPlainTextPwd } from '../../validators/user.validators';
import { Roles } from '../roles.dto';
// UserList
export class UserListRequest {
@@ -35,9 +35,8 @@ export class UserListResponse {
// UserCreate
export class UserCreateRequest extends NamePassUser {
@IsOptional()
@IsArray()
@IsString({ each: true })
roles?: Roles;
@IsStringList()
roles?: string[];
}
export class UserCreateResponse extends EUser {}
@@ -52,9 +51,8 @@ export class UserInfoResponse extends EUser {}
// UserUpdateRoles
export class UserUpdateRequest extends UsernameUser {
@IsOptional()
@IsArray()
@IsString({ each: true })
roles?: Roles;
@IsStringList()
roles?: string[];
@IsPlainTextPwd()
@IsOptional()

View File

@@ -1,47 +0,0 @@
import tuple from '../types/tuple';
import { Permission, Permissions, PermissionsList } from './permissions';
// Config
// These roles can never be removed or added to a user.
const PermanentRolesTuple = tuple('guest', 'user');
// These roles can never be modified
const ImmuteableRolesTuple = tuple('admin');
// These roles can never be removed from the server
const SystemRolesTuple = tuple(...PermanentRolesTuple, ...ImmuteableRolesTuple);
// These roles will be applied by default to new users
export const DefaultRolesList: string[] = ['user'];
// Derivatives
export const PermanentRolesList: string[] = PermanentRolesTuple;
export const ImmuteableRolesList: string[] = ImmuteableRolesTuple;
export const SystemRolesList: string[] = SystemRolesTuple;
export type SystemRole = typeof SystemRolesTuple[number];
export type SystemRoles = SystemRole[];
// Defaults
export const SystemRoleDefaults: {
[key in SystemRole]: Permissions;
} = {
guest: [Permission.ImageView, Permission.UserLogin],
user: [
Permission.ImageView,
Permission.UserMe,
Permission.UserLogin,
Permission.Settings,
Permission.ImageUpload,
],
// Grant all permissions to admin
admin: PermissionsList,
};
// Normal roles types
export type Role = SystemRole | string;
export type Roles = Role[];

View File

@@ -1,7 +1,7 @@
import { Exclude } from 'class-transformer';
import { IsArray, IsOptional, IsString } from 'class-validator';
import { Roles } from '../dto/roles.dto';
import { IsDefined, IsOptional, IsString } from 'class-validator';
import { EntityID } from '../validators/entity-id.validator';
import { IsStringList } from '../validators/string-list.validator';
import { IsPlainTextPwd, IsUsername } from '../validators/user.validators';
export class UsernameUser {
@@ -17,9 +17,9 @@ export class NamePassUser extends UsernameUser {
// Add a user object with just the username and roles for jwt
export class NameRolesUser extends UsernameUser {
@IsArray()
@IsString({ each: true })
roles: Roles;
@IsDefined()
@IsStringList()
roles: string[];
}
// Actual entity that goes in the db

View File

@@ -0,0 +1,12 @@
import {
IsArray,
IsNotEmpty,
IsString
} from 'class-validator';
import { ComposeValidators } from './compose.validator';
export const IsStringList = ComposeValidators(
IsArray(),
IsString({ each: true }),
IsNotEmpty({ each: true }),
);