mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-14 15:45:49 +01:00
apply role guard to all routes
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
node_modules
|
||||
todo.txt
|
||||
|
||||
@@ -61,7 +61,9 @@ export class SysPreferenceService {
|
||||
ESysPreferenceBackend,
|
||||
foundSysPreference,
|
||||
);
|
||||
const errors = await validate(foundSysPreference);
|
||||
const errors = await validate(foundSysPreference, {
|
||||
forbidUnknownValues: true,
|
||||
});
|
||||
if (errors.length > 0) {
|
||||
this.logger.warn(errors);
|
||||
return Fail('Invalid preference');
|
||||
@@ -85,7 +87,9 @@ export class SysPreferenceService {
|
||||
verifySysPreference.key = key as SysPreferences;
|
||||
verifySysPreference.value = value;
|
||||
|
||||
const errors = await validate(verifySysPreference);
|
||||
const errors = await validate(verifySysPreference, {
|
||||
forbidUnknownValues: true,
|
||||
});
|
||||
if (errors.length > 0) {
|
||||
this.logger.warn(errors);
|
||||
return Fail('Invalid preference');
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { CanActivate, UseGuards } from '@nestjs/common';
|
||||
import { AdminGuard } from '../managers/auth/guards/admin.guard';
|
||||
import { MainAuthGuard } from '../managers/auth/guards/main.guard';
|
||||
|
||||
export const Authenticated = (adminOnly: boolean = false) => {
|
||||
const guards: (Function | CanActivate)[] = [MainAuthGuard];
|
||||
if (adminOnly) guards.push(AdminGuard);
|
||||
|
||||
return UseGuards(...guards);
|
||||
};
|
||||
21
backend/src/decorators/roles.decorator.ts
Normal file
21
backend/src/decorators/roles.decorator.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { SetMetadata, UseGuards } from '@nestjs/common';
|
||||
import { Roles as RolesList } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { CombineDecorators } from 'picsur-shared/dist/util/decorator';
|
||||
import { LocalAuthGuard } from '../managers/auth/guards/localauth.guard';
|
||||
|
||||
export const GuestRoles = (...roles: RolesList) => {
|
||||
return SetMetadata('roles', roles);
|
||||
};
|
||||
|
||||
export const UserRoles = (...roles: RolesList) => {
|
||||
const fullRoles = [...new Set(['user', ...roles])];
|
||||
return SetMetadata('roles', fullRoles);
|
||||
};
|
||||
|
||||
// Easy to read roles
|
||||
export const Guest = () => GuestRoles();
|
||||
export const User = () => UserRoles();
|
||||
export const Admin = () => UserRoles('admin');
|
||||
|
||||
export const UseLocalAuth = () =>
|
||||
CombineDecorators(Guest(), UseGuards(LocalAuthGuard));
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestFactory, Reflector } from '@nestjs/core';
|
||||
import {
|
||||
FastifyAdapter,
|
||||
NestFastifyApplication
|
||||
@@ -10,6 +10,7 @@ import { HostConfigService } from './config/host.config.service';
|
||||
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
|
||||
import { SuccessInterceptor } from './layers/success/success.interceptor';
|
||||
import { PicsurLoggerService } from './logger/logger.service';
|
||||
import { MainAuthGuard } from './managers/auth/guards/main.guard';
|
||||
|
||||
async function bootstrap() {
|
||||
const fastifyAdapter = new FastifyAdapter();
|
||||
@@ -32,6 +33,7 @@ async function bootstrap() {
|
||||
forbidUnknownValues: true,
|
||||
}),
|
||||
);
|
||||
app.useGlobalGuards(new MainAuthGuard(new Reflector()));
|
||||
|
||||
app.useLogger(app.get(PicsurLoggerService));
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { AuthManagerService } from './auth.service';
|
||||
import { GuestStrategy } from './guards/guest.strategy';
|
||||
import { JwtStrategy } from './guards/jwt.strategy';
|
||||
import { LocalAuthStrategy } from './guards/localauth.strategy';
|
||||
import { GuestService } from './guest.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -32,6 +33,7 @@ import { LocalAuthStrategy } from './guards/localauth.strategy';
|
||||
JwtStrategy,
|
||||
GuestStrategy,
|
||||
JwtSecretProvider,
|
||||
GuestService,
|
||||
],
|
||||
exports: [AuthManagerService],
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Request } from 'express';
|
||||
import { ParamsDictionary } from 'express-serve-static-core';
|
||||
import { Strategy } from 'passport-strategy';
|
||||
import { ParsedQs } from 'qs';
|
||||
import { GuestService } from '../guest.service';
|
||||
|
||||
type ReqType = Request<
|
||||
ParamsDictionary,
|
||||
@@ -18,10 +19,9 @@ class GuestPassportStrategy extends Strategy {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override authenticate(req: ReqType, options?: any): void {
|
||||
const user = this.validate(req);
|
||||
req['user'] = user;
|
||||
this.pass();
|
||||
override async authenticate(req: ReqType, options?: any) {
|
||||
const user = await this.validate(req);
|
||||
this.success(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,11 @@ export class GuestStrategy extends PassportStrategy(
|
||||
) {
|
||||
private readonly logger = new Logger('GuestStrategy');
|
||||
|
||||
constructor(private guestService: GuestService) {
|
||||
super();
|
||||
}
|
||||
|
||||
override async validate(payload: any) {
|
||||
// TODO: add guest user
|
||||
return;
|
||||
return this.guestService.createGuest();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,84 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
Logger
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { isArray, isEnum, isString, validate } from 'class-validator';
|
||||
import { Roles, RolesList } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { Fail, Failable, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { EUserBackend } from '../../../models/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {}
|
||||
export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
|
||||
private readonly logger = new Logger('MainAuthGuard');
|
||||
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
override async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const result = await super.canActivate(context);
|
||||
if (result !== true) {
|
||||
this.logger.error('Main Auth has denied access, this should not happen');
|
||||
return false;
|
||||
}
|
||||
|
||||
const user = await this.validateUser(
|
||||
context.switchToHttp().getRequest().user,
|
||||
);
|
||||
|
||||
const roles = this.extractRoles(context);
|
||||
if (HasFailed(roles)) {
|
||||
this.logger.warn(roles.getReason());
|
||||
return false;
|
||||
}
|
||||
|
||||
// User must have all roles
|
||||
return roles.every((role) => user.roles.includes(role));
|
||||
}
|
||||
|
||||
private extractRoles(context: ExecutionContext): Failable<Roles> {
|
||||
const handlerName = context.getHandler().name;
|
||||
const roles =
|
||||
this.reflector.get<Roles>('roles', context.getHandler()) ??
|
||||
this.reflector.get<Roles>('roles', context.getClass());
|
||||
|
||||
if (roles === undefined) {
|
||||
return Fail(
|
||||
`${handlerName} does not have any roles defined, denying access`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.isRolesArray(roles)) {
|
||||
return Fail(`Roles for ${handlerName} is not a string array`);
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
private isRolesArray(value: any): value is Roles {
|
||||
if (!isArray(value)) return false;
|
||||
if (!value.every((item: unknown) => isString(item))) return false;
|
||||
if (!value.every((item: string) => isEnum(item, RolesList))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private async validateUser(user: EUserBackend): Promise<EUserBackend> {
|
||||
const userClass = plainToClass(EUserBackend, user);
|
||||
const errors = await validate(userClass, {
|
||||
forbidUnknownValues: true,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
this.logger.error(
|
||||
'Invalid user object, where it should always be valid: ' + errors,
|
||||
);
|
||||
throw new InternalServerErrorException();
|
||||
}
|
||||
return userClass;
|
||||
}
|
||||
}
|
||||
|
||||
19
backend/src/managers/auth/guest.service.ts
Normal file
19
backend/src/managers/auth/guest.service.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
|
||||
import { EUserBackend } from '../../models/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class GuestService {
|
||||
public createGuest(): EUserBackend {
|
||||
const guest = new EUserBackend();
|
||||
guest.id = -1;
|
||||
guest.roles = this.createGuestRoles();
|
||||
guest.username = 'guest';
|
||||
|
||||
return guest;
|
||||
}
|
||||
|
||||
private createGuestRoles(): Roles {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,7 @@ import {
|
||||
Get,
|
||||
InternalServerErrorException,
|
||||
Post,
|
||||
Request,
|
||||
UseGuards
|
||||
Request
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
AuthDeleteRequest,
|
||||
@@ -14,9 +13,8 @@ import {
|
||||
AuthRegisterRequest
|
||||
} from 'picsur-shared/dist/dto/auth.dto';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { Authenticated } from '../../../decorators/authenticated';
|
||||
import { Admin, UseLocalAuth, User } from '../../../decorators/roles.decorator';
|
||||
import { AuthManagerService } from '../../../managers/auth/auth.service';
|
||||
import { LocalAuthGuard } from '../../../managers/auth/guards/localauth.guard';
|
||||
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
|
||||
|
||||
@Controller('api/auth')
|
||||
@@ -24,7 +22,7 @@ export class AuthController {
|
||||
constructor(private authService: AuthManagerService) {}
|
||||
|
||||
@Post('login')
|
||||
@UseGuards(LocalAuthGuard)
|
||||
@UseLocalAuth()
|
||||
async login(@Request() req: AuthFasityRequest) {
|
||||
const response: AuthLoginResponse = {
|
||||
jwt_token: await this.authService.createToken(req.user),
|
||||
@@ -34,7 +32,7 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Post('create')
|
||||
@Authenticated(true)
|
||||
@Admin()
|
||||
async register(
|
||||
@Request() req: AuthFasityRequest,
|
||||
@Body() register: AuthRegisterRequest,
|
||||
@@ -56,7 +54,7 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Post('delete')
|
||||
@Authenticated(true)
|
||||
@Admin()
|
||||
async delete(
|
||||
@Request() req: AuthFasityRequest,
|
||||
@Body() deleteData: AuthDeleteRequest,
|
||||
@@ -71,7 +69,7 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Get('list')
|
||||
@Authenticated(true)
|
||||
@Admin()
|
||||
async listUsers(@Request() req: AuthFasityRequest) {
|
||||
const users = this.authService.listUsers();
|
||||
if (HasFailed(users)) {
|
||||
@@ -83,7 +81,7 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
@Authenticated()
|
||||
@User()
|
||||
async me(@Request() req: AuthFasityRequest) {
|
||||
const meResponse: AuthMeResponse = new AuthMeResponse();
|
||||
meResponse.user = req.user;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Controller, Get, Request, UseGuards } from '@nestjs/common';
|
||||
import { MainAuthGuard } from '../../../managers/auth/guards/main.guard';
|
||||
import { Controller, Get, Request } from '@nestjs/common';
|
||||
import { Guest } from '../../../decorators/roles.decorator';
|
||||
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
|
||||
|
||||
@Controller('api/experiment')
|
||||
export class ExperimentController {
|
||||
|
||||
@Get()
|
||||
@UseGuards(MainAuthGuard)
|
||||
@Guest()
|
||||
async testRoute(@Request() req: AuthFasityRequest) {
|
||||
console.log("calledroutes")
|
||||
return {
|
||||
message: req.user,
|
||||
};
|
||||
|
||||
@@ -12,10 +12,10 @@ import {
|
||||
} from 'picsur-shared/dist/dto/syspreferences.dto';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service';
|
||||
import { Authenticated } from '../../../decorators/authenticated';
|
||||
import { Admin } from '../../../decorators/roles.decorator';
|
||||
|
||||
@Controller('api/pref')
|
||||
@Authenticated(true)
|
||||
@Admin()
|
||||
export class PrefController {
|
||||
constructor(private prefService: SysPreferenceService) {}
|
||||
|
||||
|
||||
@@ -12,11 +12,13 @@ import {
|
||||
import { isHash } from 'class-validator';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { MultiPart } from '../../decorators/multipart';
|
||||
import { MultiPart } from '../../decorators/multipart.decorator';
|
||||
import { Guest } from '../../decorators/roles.decorator';
|
||||
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
|
||||
import { ImageUploadDto } from '../../models/dto/imageroute.dto';
|
||||
|
||||
@Controller('i')
|
||||
@Guest()
|
||||
export class ImageController {
|
||||
constructor(private readonly imagesService: ImageManagerService) {}
|
||||
|
||||
@@ -51,6 +53,7 @@ export class ImageController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
//@User()
|
||||
async uploadImage(
|
||||
@Req() req: FastifyRequest,
|
||||
@MultiPart(ImageUploadDto) multipart: ImageUploadDto,
|
||||
|
||||
9
shared/src/util/decorator.ts
Normal file
9
shared/src/util/decorator.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
type FCDecorator = MethodDecorator & ClassDecorator;
|
||||
|
||||
export function CombineDecorators(...decorators: FCDecorator[]) {
|
||||
return (target: any, key: string, descriptor: PropertyDescriptor) => {
|
||||
decorators.forEach(decorator => {
|
||||
decorator(target, key, descriptor);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user