From 1febcd8147f798fe4f113e4533149c1316f9bda0 Mon Sep 17 00:00:00 2001 From: rubikscraft Date: Sat, 12 Mar 2022 16:25:15 +0100 Subject: [PATCH] add role api --- .../src/collections/roledb/roledb.module.ts | 4 +- .../src/collections/roledb/roledb.service.ts | 28 +++-- backend/src/routes/api/api.module.ts | 4 +- .../src/routes/api/auth/auth.controller.ts | 20 +++- .../src/routes/api/roles/roles.controller.ts | 113 ++++++++++++++++++ backend/src/routes/api/roles/roles.module.ts | 9 ++ shared/src/dto/api/auth.dto.ts | 6 + shared/src/dto/api/info.dto.ts | 4 +- shared/src/dto/api/roles.dto.ts | 26 ++++ shared/src/dto/permissions.ts | 1 + shared/src/entities/image.entity.ts | 3 +- shared/src/entities/role.entity.ts | 4 +- shared/src/entities/syspreference.entity.ts | 4 +- shared/src/entities/user.entity.ts | 5 +- 14 files changed, 212 insertions(+), 19 deletions(-) create mode 100644 backend/src/routes/api/roles/roles.controller.ts create mode 100644 backend/src/routes/api/roles/roles.module.ts create mode 100644 shared/src/dto/api/roles.dto.ts diff --git a/backend/src/collections/roledb/roledb.module.ts b/backend/src/collections/roledb/roledb.module.ts index 1f838e6..7410ae4 100644 --- a/backend/src/collections/roledb/roledb.module.ts +++ b/backend/src/collections/roledb/roledb.module.ts @@ -35,8 +35,8 @@ export class RolesModule implements OnModuleInit { } private async nukeRoles() { - this.logger.error('Nuking all roles'); - const result = this.rolesService.nuke(true); + this.logger.error('Nuking system roles'); + const result = this.rolesService.nukeSystemRoles(true); if (HasFailed(result)) { this.logger.error(`Failed to nuke roles because: ${result.getReason()}`); } diff --git a/backend/src/collections/roledb/roledb.service.ts b/backend/src/collections/roledb/roledb.service.ts index 37f5d7d..8618c5a 100644 --- a/backend/src/collections/roledb/roledb.service.ts +++ b/backend/src/collections/roledb/roledb.service.ts @@ -14,7 +14,7 @@ import { HasFailed, HasSuccess } from 'picsur-shared/dist/types'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { ERoleBackend } from '../../models/entities/role.entity'; @Injectable() @@ -79,7 +79,7 @@ export class RolesService { public async addPermissions( role: string | ERoleBackend, permissions: Permissions, - ): AsyncFailable { + ): AsyncFailable { const roleToModify = await this.resolve(role); if (HasFailed(roleToModify)) return roleToModify; @@ -94,7 +94,7 @@ export class RolesService { public async removePermissions( role: string | ERoleBackend, permissions: Permissions, - ): AsyncFailable { + ): AsyncFailable { const roleToModify = await this.resolve(role); if (HasFailed(roleToModify)) return roleToModify; @@ -109,7 +109,7 @@ export class RolesService { role: string | ERoleBackend, permissions: Permissions, allowImmutable: boolean = false, - ): AsyncFailable { + ): AsyncFailable { const roleToModify = await this.resolve(role); if (HasFailed(roleToModify)) return roleToModify; @@ -120,12 +120,10 @@ export class RolesService { roleToModify.permissions = permissions; try { - await this.rolesRepository.save(roleToModify); + return await this.rolesRepository.save(roleToModify); } catch (e: any) { return Fail(e?.message); } - - return true; } public async findOne(name: string): AsyncFailable { @@ -141,14 +139,26 @@ export class RolesService { } } + public async findAll(): AsyncFailable { + try { + const found = await this.rolesRepository.find(); + if (!found) return Fail('No roles found'); + return found as ERoleBackend[]; + } catch (e: any) { + return Fail(e?.message); + } + } + public async exists(username: string): Promise { return HasSuccess(await this.findOne(username)); } - public async nuke(iamsure: boolean = false): AsyncFailable { + public async nukeSystemRoles(iamsure: boolean = false): AsyncFailable { if (!iamsure) return Fail('Nuke aborted'); try { - await this.rolesRepository.delete({}); + await this.rolesRepository.delete({ + name: In(SystemRolesList), + }); } catch (e: any) { return Fail(e?.message); } diff --git a/backend/src/routes/api/api.module.ts b/backend/src/routes/api/api.module.ts index b22185e..1ac5ec1 100644 --- a/backend/src/routes/api/api.module.ts +++ b/backend/src/routes/api/api.module.ts @@ -1,8 +1,9 @@ import { Module } from '@nestjs/common'; import { AuthModule } from './auth/auth.module'; import { ExperimentModule } from './experiment/experiment.module'; -import { PrefModule } from './pref/pref.module'; import { InfoModule } from './info/info.module'; +import { PrefModule } from './pref/pref.module'; +import { RolesApiModule } from './roles/roles.module'; @Module({ imports: [ @@ -10,6 +11,7 @@ import { InfoModule } from './info/info.module'; PrefModule, ExperimentModule, InfoModule, + RolesApiModule, ] }) export class PicsurApiModule {} diff --git a/backend/src/routes/api/auth/auth.controller.ts b/backend/src/routes/api/auth/auth.controller.ts index 0d3c796..b441a83 100644 --- a/backend/src/routes/api/auth/auth.controller.ts +++ b/backend/src/routes/api/auth/auth.controller.ts @@ -7,6 +7,7 @@ import { Post, Request } from '@nestjs/common'; +import { AuthUserInfoRequest } from 'picsur-shared/dist/dto/api/auth.dto'; import { AuthDeleteRequest, AuthLoginResponse, @@ -15,14 +16,17 @@ import { } from 'picsur-shared/dist/dto/auth.dto'; import { HasFailed } from 'picsur-shared/dist/types'; import { UsersService } from '../../../collections/userdb/userdb.service'; -import { RequiredPermissions, UseLocalAuth } from '../../../decorators/permissions.decorator'; +import { + RequiredPermissions, + UseLocalAuth +} from '../../../decorators/permissions.decorator'; import { AuthManagerService } from '../../../managers/auth/auth.service'; import AuthFasityRequest from '../../../models/dto/authrequest.dto'; @Controller('api/auth') export class AuthController { private readonly logger = new Logger('AuthController'); - + constructor( private usersService: UsersService, private authService: AuthManagerService, @@ -75,6 +79,18 @@ export class AuthController { return user; } + @Post('info') + @RequiredPermissions('user-manage') + async getUser(@Body() body: AuthUserInfoRequest) { + const user = await this.usersService.findOne(body.username); + if (HasFailed(user)) { + this.logger.warn(user.getReason()); + throw new InternalServerErrorException('Could not find user'); + } + + return user; + } + @Get('list') @RequiredPermissions('user-manage') async listUsers(@Request() req: AuthFasityRequest) { diff --git a/backend/src/routes/api/roles/roles.controller.ts b/backend/src/routes/api/roles/roles.controller.ts new file mode 100644 index 0000000..d0318fd --- /dev/null +++ b/backend/src/routes/api/roles/roles.controller.ts @@ -0,0 +1,113 @@ +import { + Body, + Controller, + Get, + InternalServerErrorException, + Logger, + Post +} from '@nestjs/common'; +import { + RoleCreateRequest, + RoleDeleteRequest, + RoleInfoRequest, + RoleUpdateRequest +} from 'picsur-shared/dist/dto/api/roles.dto'; +import { HasFailed } from 'picsur-shared/dist/types'; +import { RolesService } from '../../../collections/roledb/roledb.service'; +import { RequiredPermissions } from '../../../decorators/permissions.decorator'; + +@Controller('api/roles') +@RequiredPermissions('role-manage') +export class RolesController { + private readonly logger = new Logger('RolesController'); + + constructor(private rolesService: RolesService) {} + + @Get('/list') + getRoles() { + const roles = this.rolesService.findAll(); + if (HasFailed(roles)) { + this.logger.warn(roles.getReason()); + throw new InternalServerErrorException('Could not list roles'); + } + + return roles; + } + + @Post('/info') + getRole(@Body() body: RoleInfoRequest) { + const role = this.rolesService.findOne(body.name); + if (HasFailed(role)) { + this.logger.warn(role.getReason()); + throw new InternalServerErrorException('Could not find role'); + } + + return role; + } + + @Post('/permissions/set') + updateRole(@Body() body: RoleUpdateRequest) { + const updatedRole = this.rolesService.setPermissions( + body.name, + body.permissions, + ); + if (HasFailed(updatedRole)) { + this.logger.warn(updatedRole.getReason()); + throw new InternalServerErrorException('Could not set role permissions'); + } + + return updatedRole; + } + + @Post('/permissions/add') + addPermissions(@Body() body: RoleUpdateRequest) { + const updatedRole = this.rolesService.addPermissions( + body.name, + body.permissions, + ); + if (HasFailed(updatedRole)) { + this.logger.warn(updatedRole.getReason()); + throw new InternalServerErrorException('Could not add role permissions'); + } + + return updatedRole; + } + + @Post('/permissions/remove') + removePermissions(@Body() body: RoleUpdateRequest) { + const updatedRole = this.rolesService.removePermissions( + body.name, + body.permissions, + ); + if (HasFailed(updatedRole)) { + this.logger.warn(updatedRole.getReason()); + throw new InternalServerErrorException( + 'Could not remove role permissions', + ); + } + + return updatedRole; + } + + @Post('/create') + createRole(@Body() role: RoleCreateRequest) { + const newRole = this.rolesService.create(role.name, role.permissions); + if (HasFailed(newRole)) { + this.logger.warn(newRole.getReason()); + throw new InternalServerErrorException('Could not create role'); + } + + return newRole; + } + + @Post('/delete') + deleteRole(@Body() role: RoleDeleteRequest) { + const deletedRole = this.rolesService.delete(role.name); + if (HasFailed(deletedRole)) { + this.logger.warn(deletedRole.getReason()); + throw new InternalServerErrorException('Could not delete role'); + } + + return deletedRole; + } +} diff --git a/backend/src/routes/api/roles/roles.module.ts b/backend/src/routes/api/roles/roles.module.ts new file mode 100644 index 0000000..15b01a3 --- /dev/null +++ b/backend/src/routes/api/roles/roles.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { RolesModule } from '../../../collections/roledb/roledb.module'; +import { RolesController } from './roles.controller'; + +@Module({ + imports: [RolesModule], + controllers: [RolesController], +}) +export class RolesApiModule {} diff --git a/shared/src/dto/api/auth.dto.ts b/shared/src/dto/api/auth.dto.ts index ada92f2..e9d1fb6 100644 --- a/shared/src/dto/api/auth.dto.ts +++ b/shared/src/dto/api/auth.dto.ts @@ -66,3 +66,9 @@ export class AuthMeResponse { @IsDefined() newJwtToken: string; } + +export class AuthUserInfoRequest { + @IsString() + @IsNotEmpty() + username: string; +} diff --git a/shared/src/dto/api/info.dto.ts b/shared/src/dto/api/info.dto.ts index 3dd9d8b..64ab67b 100644 --- a/shared/src/dto/api/info.dto.ts +++ b/shared/src/dto/api/info.dto.ts @@ -1,9 +1,11 @@ -import { IsDefined } from 'class-validator'; +import { IsBoolean, IsDefined } from 'class-validator'; export class InfoResponse { + @IsBoolean() @IsDefined() production: boolean; + @IsBoolean() @IsDefined() demo: boolean; } diff --git a/shared/src/dto/api/roles.dto.ts b/shared/src/dto/api/roles.dto.ts new file mode 100644 index 0000000..426f026 --- /dev/null +++ b/shared/src/dto/api/roles.dto.ts @@ -0,0 +1,26 @@ +import { IsArray, IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { ERole } from '../../entities/role.entity'; +import { Permissions, PermissionsList } from '../permissions'; + +export class RoleInfoRequest { + @IsNotEmpty() + @IsString() + name: string; +} + +export class RoleUpdateRequest { + @IsNotEmpty() + @IsString() + name: string; + + @IsArray() + @IsEnum(PermissionsList, { each: true }) + permissions: Permissions; +} + +export class RoleCreateRequest extends ERole {} + +export class RoleDeleteRequest { + @IsNotEmpty() + name: string; +} diff --git a/shared/src/dto/permissions.ts b/shared/src/dto/permissions.ts index 03cd694..7ada78c 100644 --- a/shared/src/dto/permissions.ts +++ b/shared/src/dto/permissions.ts @@ -9,6 +9,7 @@ const PermissionsTuple = tuple( 'user-register', // Ability to register 'user-view', // Ability to view user info, only granted if logged in 'user-manage', + 'role-manage', 'syspref-manage', ); diff --git a/shared/src/entities/image.entity.ts b/shared/src/entities/image.entity.ts index 155bf23..b13b011 100644 --- a/shared/src/entities/image.entity.ts +++ b/shared/src/entities/image.entity.ts @@ -1,9 +1,10 @@ import { Exclude } from 'class-transformer'; -import { IsDefined, IsEnum, IsHash, IsOptional } from 'class-validator'; +import { IsDefined, IsEnum, IsHash, IsInt, IsOptional } from 'class-validator'; import { SupportedMime, SupportedMimes } from '../dto/mimes.dto'; export class EImage { @IsOptional() + @IsInt() id?: number; @IsHash('sha256') diff --git a/shared/src/entities/role.entity.ts b/shared/src/entities/role.entity.ts index c55d62a..668dfd9 100644 --- a/shared/src/entities/role.entity.ts +++ b/shared/src/entities/role.entity.ts @@ -1,11 +1,13 @@ -import { IsArray, IsEnum, IsNotEmpty, IsOptional } from 'class-validator'; +import { IsArray, IsEnum, IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { Permissions, PermissionsList } from '../dto/permissions'; export class ERole { @IsOptional() + @IsInt() id?: number; @IsNotEmpty() + @IsString() name: string; @IsArray() diff --git a/shared/src/entities/syspreference.entity.ts b/shared/src/entities/syspreference.entity.ts index a638d34..df70b0a 100644 --- a/shared/src/entities/syspreference.entity.ts +++ b/shared/src/entities/syspreference.entity.ts @@ -1,8 +1,9 @@ -import { IsEnum, IsNotEmpty, IsOptional } from 'class-validator'; +import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { SysPreferences } from '../dto/syspreferences.dto'; export class ESysPreference { @IsOptional() + @IsInt() id?: number; @IsNotEmpty() @@ -10,5 +11,6 @@ export class ESysPreference { key: SysPreferences; @IsNotEmpty() + @IsString() value: string; } diff --git a/shared/src/entities/user.entity.ts b/shared/src/entities/user.entity.ts index 601c3bd..a074acf 100644 --- a/shared/src/entities/user.entity.ts +++ b/shared/src/entities/user.entity.ts @@ -1,6 +1,6 @@ import { Exclude } from 'class-transformer'; import { - IsArray, IsNotEmpty, + IsArray, IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; @@ -8,9 +8,11 @@ import { Roles } from '../dto/roles.dto'; export class EUser { @IsOptional() + @IsInt() id?: number; @IsNotEmpty() + @IsString() username: string; @IsArray() @@ -19,5 +21,6 @@ export class EUser { @IsOptional() @Exclude() + @IsString() password?: string; }