From 52434efdd7f85b4fcd9dea428e1800dd5eb05d89 Mon Sep 17 00:00:00 2001 From: rubikscraft Date: Sat, 12 Mar 2022 21:13:58 +0100 Subject: [PATCH] add permission endpoint --- .../src/decorators/permissions.decorator.ts | 2 +- .../src/routes/api/auth/user.controller.ts | 22 +++++--- .../src/routes/api/info/info.controller.ts | 4 +- frontend/src/app/api/api.module.ts | 9 +++- frontend/src/app/api/permission.service.ts | 50 +++++++++++++++++++ frontend/src/app/api/user.service.ts | 5 +- shared/src/dto/api/user.dto.ts | 11 ++-- shared/src/dto/permissions.ts | 2 +- 8 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 frontend/src/app/api/permission.service.ts diff --git a/backend/src/decorators/permissions.decorator.ts b/backend/src/decorators/permissions.decorator.ts index 7fe7a5e..5532641 100644 --- a/backend/src/decorators/permissions.decorator.ts +++ b/backend/src/decorators/permissions.decorator.ts @@ -8,7 +8,7 @@ export const RequiredPermissions = (...permissions: Permissions) => { }; // Easy to read roles -export const NoAuth = () => RequiredPermissions(); +export const NoPermissions = () => RequiredPermissions(); export const UseLocalAuth = (...permissions: Permissions) => CombineDecorators( diff --git a/backend/src/routes/api/auth/user.controller.ts b/backend/src/routes/api/auth/user.controller.ts index cb33e37..5c4c076 100644 --- a/backend/src/routes/api/auth/user.controller.ts +++ b/backend/src/routes/api/auth/user.controller.ts @@ -14,6 +14,7 @@ import { UserInfoResponse, UserListResponse, UserLoginResponse, + UserMePermissionsResponse, UserMeResponse, UserRegisterRequest, UserRegisterResponse, @@ -23,6 +24,7 @@ import { import { HasFailed } from 'picsur-shared/dist/types'; import { UsersService } from '../../../collections/userdb/userdb.service'; import { + NoPermissions, RequiredPermissions, UseLocalAuth } from '../../../decorators/permissions.decorator'; @@ -94,7 +96,7 @@ export class UserController { body.username, body.roles, ); - + if (HasFailed(updatedUser)) { this.logger.warn(updatedUser.getReason()); throw new InternalServerErrorException('Could not update user'); @@ -133,16 +135,24 @@ export class UserController { @Get('me') @RequiredPermissions('user-view') async me(@Request() req: AuthFasityRequest): Promise { + return { + user: req.user, + token: await this.authService.createToken(req.user), + }; + } + + // You can always check your permissions + @Get('me/permissions') + @NoPermissions() + async refresh( + @Request() req: AuthFasityRequest, + ): Promise { const permissions = await this.usersService.getPermissions(req.user); if (HasFailed(permissions)) { this.logger.warn(permissions.getReason()); throw new InternalServerErrorException('Could not get permissions'); } - return { - user: req.user, - permissions, - newJwtToken: await this.authService.createToken(req.user), - }; + return { permissions }; } } diff --git a/backend/src/routes/api/info/info.controller.ts b/backend/src/routes/api/info/info.controller.ts index 0ac26f7..5027d80 100644 --- a/backend/src/routes/api/info/info.controller.ts +++ b/backend/src/routes/api/info/info.controller.ts @@ -1,14 +1,14 @@ import { Controller, Get } from '@nestjs/common'; import { InfoResponse } from 'picsur-shared/dist/dto/api/info.dto'; import { HostConfigService } from '../../../config/host.config.service'; -import { NoAuth } from '../../../decorators/permissions.decorator'; +import { NoPermissions } from '../../../decorators/permissions.decorator'; @Controller('api/info') export class InfoController { constructor(private hostConfig: HostConfigService) {} @Get() - @NoAuth() + @NoPermissions() async getInfo(): Promise { return { demo: this.hostConfig.isDemo(), diff --git a/frontend/src/app/api/api.module.ts b/frontend/src/app/api/api.module.ts index 849f682..e090ed7 100644 --- a/frontend/src/app/api/api.module.ts +++ b/frontend/src/app/api/api.module.ts @@ -3,10 +3,17 @@ import { NgModule } from '@angular/core'; import { ApiService } from './api.service'; import { ImageService } from './image.service'; import { KeyService } from './key.service'; +import { PermissionService } from './permission.service'; import { UserService } from './user.service'; @NgModule({ - providers: [ApiService, ImageService, UserService, KeyService], + providers: [ + ApiService, + ImageService, + UserService, + PermissionService, + KeyService, + ], imports: [CommonModule], }) export class ApiModule {} diff --git a/frontend/src/app/api/permission.service.ts b/frontend/src/app/api/permission.service.ts new file mode 100644 index 0000000..c0aa32b --- /dev/null +++ b/frontend/src/app/api/permission.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@angular/core'; +import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; +import { UserMePermissionsResponse } from 'picsur-shared/dist/dto/api/user.dto'; +import { Permissions } from 'picsur-shared/dist/dto/permissions'; +import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types'; +import { BehaviorSubject } from 'rxjs'; +import { ApiService } from './api.service'; +import { UserService } from './user.service'; + +@Injectable() +export class PermissionService { + private readonly logger = console; + + constructor(private userService: UserService, private api: ApiService) { + this.onUser(); + } + + public get livePermissions() { + return this.permissionsSubject; + } + + public get permissions() { + return this.permissionsSubject.getValue(); + } + + private permissionsSubject = new BehaviorSubject([]); + + @AutoUnsubscribe() + private onUser() { + return this.userService.liveUser.subscribe(async (user) => { + const permissions = await this.fetchPermissions(); + if (HasFailed(permissions)) { + this.logger.warn(permissions.getReason()); + return; + } + + this.permissionsSubject.next(permissions); + }); + } + + private async fetchPermissions(): AsyncFailable { + const got = await this.api.get( + UserMePermissionsResponse, + '/api/user/me/permissions' + ); + if (HasFailed(got)) return got; + + return got.permissions; + } +} diff --git a/frontend/src/app/api/user.service.ts b/frontend/src/app/api/user.service.ts index 0184e70..b3d1dc9 100644 --- a/frontend/src/app/api/user.service.ts +++ b/frontend/src/app/api/user.service.ts @@ -4,8 +4,7 @@ import { validate } from 'class-validator'; import jwt_decode from 'jwt-decode'; import { UserLoginRequest, - UserLoginResponse, - UserMeResponse + UserLoginResponse, UserMeResponse } from 'picsur-shared/dist/dto/api/user.dto'; import { JwtDataDto } from 'picsur-shared/dist/dto/jwt.dto'; import { EUser } from 'picsur-shared/dist/entities/user.entity'; @@ -117,7 +116,7 @@ export class UserService { const got = await this.api.get(UserMeResponse, '/api/user/me'); if (HasFailed(got)) return got; - this.key.set(got.newJwtToken); + this.key.set(got.token); return got.user; } } diff --git a/shared/src/dto/api/user.dto.ts b/shared/src/dto/api/user.dto.ts index dfe7c64..eae7af6 100644 --- a/shared/src/dto/api/user.dto.ts +++ b/shared/src/dto/api/user.dto.ts @@ -90,14 +90,17 @@ export class UserMeResponse { @Type(() => EUser) user: EUser; + @IsString() + @IsDefined() + token: string; +} + +// UserMePermissions +export class UserMePermissionsResponse { @IsDefined() @IsArray() @IsEnum(PermissionsList, { each: true }) permissions: Permissions; - - @IsString() - @IsDefined() - newJwtToken: string; } // UserUpdateRoles diff --git a/shared/src/dto/permissions.ts b/shared/src/dto/permissions.ts index 7ada78c..d1b11d1 100644 --- a/shared/src/dto/permissions.ts +++ b/shared/src/dto/permissions.ts @@ -7,8 +7,8 @@ const PermissionsTuple = tuple( 'image-upload', 'user-login', // Ability to log in 'user-register', // Ability to register - 'user-view', // Ability to view user info, only granted if logged in 'user-manage', + 'user-view', // Ability to view user details and refresh token 'role-manage', 'syspref-manage', );