diff --git a/backend/src/collections/preference-db/sys-preference-db.service.ts b/backend/src/collections/preference-db/sys-preference-db.service.ts index 6433aec..00c8f8f 100644 --- a/backend/src/collections/preference-db/sys-preference-db.service.ts +++ b/backend/src/collections/preference-db/sys-preference-db.service.ts @@ -113,7 +113,8 @@ export class SysPreferenceService { ): AsyncFailable { let pref = await this.getPreference(key); if (HasFailed(pref)) return pref; - if (pref.type !== type) return Fail(FT.UsrValidation, 'Invalid preference type'); + if (pref.type !== type) + return Fail(FT.UsrValidation, 'Invalid preference type'); return pref.value; } diff --git a/backend/src/decorators/image-id/image-full-id.pipe.ts b/backend/src/decorators/image-id/image-full-id.pipe.ts index 850119f..8cf542c 100644 --- a/backend/src/decorators/image-id/image-full-id.pipe.ts +++ b/backend/src/decorators/image-id/image-full-id.pipe.ts @@ -1,10 +1,9 @@ import { - ArgumentMetadata, - BadRequestException, - Injectable, - PipeTransform, + ArgumentMetadata, Injectable, + PipeTransform } from '@nestjs/common'; import { Ext2Mime } from 'picsur-shared/dist/dto/mimes.dto'; +import { Fail, FT } from 'picsur-shared/dist/types'; import { UUIDRegex } from 'picsur-shared/dist/util/common-regex'; import { ImageFullId } from '../../models/constants/image-full-id.const'; @@ -15,23 +14,23 @@ export class ImageFullIdPipe implements PipeTransform { if (split.length === 2) { const [id, ext] = split; if (!UUIDRegex.test(id)) - throw new BadRequestException('Invalid image identifier'); + throw Fail(FT.UsrValidation, 'Invalid image identifier'); const mime = Ext2Mime(ext); if (mime === undefined) - throw new BadRequestException('Invalid image identifier'); + throw Fail(FT.UsrValidation, 'Invalid image identifier'); return { type: 'normal', id, ext, mime }; } else if (split.length === 1) { const [id] = split; if (!UUIDRegex.test(id)) - throw new BadRequestException('Invalid image identifier'); + throw Fail(FT.UsrValidation, 'Invalid image identifier'); return { type: 'original', id, ext: null, mime: null }; } else { - throw new BadRequestException('Invalid image identifier'); + throw Fail(FT.UsrValidation, 'Invalid image identifier'); } } } diff --git a/backend/src/decorators/image-id/image-id.pipe.ts b/backend/src/decorators/image-id/image-id.pipe.ts index 206903d..10c5f8f 100644 --- a/backend/src/decorators/image-id/image-id.pipe.ts +++ b/backend/src/decorators/image-id/image-id.pipe.ts @@ -1,15 +1,14 @@ import { - ArgumentMetadata, - BadRequestException, - Injectable, - PipeTransform, + ArgumentMetadata, Injectable, + PipeTransform } from '@nestjs/common'; +import { Fail, FT } from 'picsur-shared/dist/types'; import { UUIDRegex } from 'picsur-shared/dist/util/common-regex'; @Injectable() export class ImageIdPipe implements PipeTransform { transform(value: string, metadata: ArgumentMetadata): string { if (UUIDRegex.test(value)) return value; - throw new BadRequestException('Invalid image id'); + throw Fail(FT.UsrValidation, 'Invalid image id'); } } diff --git a/backend/src/decorators/multipart/multipart.pipe.ts b/backend/src/decorators/multipart/multipart.pipe.ts index e73a6ca..d381389 100644 --- a/backend/src/decorators/multipart/multipart.pipe.ts +++ b/backend/src/decorators/multipart/multipart.pipe.ts @@ -1,15 +1,12 @@ import { MultipartFields, MultipartFile } from '@fastify/multipart'; import { - ArgumentMetadata, - BadRequestException, - Injectable, - InternalServerErrorException, + ArgumentMetadata, Injectable, Logger, PipeTransform, Scope } from '@nestjs/common'; import { FastifyRequest } from 'fastify'; -import { HasFailed } from 'picsur-shared/dist/types'; +import { Fail, FT, HasFailed } from 'picsur-shared/dist/types'; import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto'; import { MultipartConfigService } from '../../config/early/multipart.config.service'; import { @@ -32,11 +29,11 @@ export class MultiPartPipe implements PipeTransform { let zodSchema = (metadata?.metatype as ZodDtoStatic)?.zodSchema; if (!zodSchema) { this.logger.error('Invalid scheme on multipart body'); - throw new InternalServerErrorException('Invalid scheme on backend'); + throw Fail(FT.Internal, 'Invalid scheme on backend'); } let multipartData = {}; - if (!req.isMultipart()) throw new BadRequestException('Invalid file'); + if (!req.isMultipart()) throw Fail(FT.UsrValidation, 'Invalid file'); // Fetch all fields from the request let fields: MultipartFields | null = null; @@ -49,7 +46,7 @@ export class MultiPartPipe implements PipeTransform { } catch (e) { this.logger.warn(e); } - if (!fields) throw new BadRequestException('Invalid file'); + if (!fields) throw Fail(FT.UsrValidation, 'Invalid file'); // Loop over every formfield that was sent for (const key of Object.keys(fields)) { @@ -66,10 +63,7 @@ export class MultiPartPipe implements PipeTransform { ); } else { const file = await CreateMultiPartFileDto(fields[key] as MultipartFile); - if (HasFailed(file)) { - this.logger.error(file.getReason()); - throw new InternalServerErrorException('Invalid file'); - } + if (HasFailed(file)) throw file; (multipartData as any)[key] = file; } } @@ -78,7 +72,7 @@ export class MultiPartPipe implements PipeTransform { const result = zodSchema.safeParse(multipartData); if (!result.success) { this.logger.warn(result.error); - throw new BadRequestException('Invalid file'); + throw Fail(FT.UsrValidation, 'Invalid file'); } return result.data; diff --git a/backend/src/decorators/multipart/postfile.pipe.ts b/backend/src/decorators/multipart/postfile.pipe.ts index 81494c3..57d96d9 100644 --- a/backend/src/decorators/multipart/postfile.pipe.ts +++ b/backend/src/decorators/multipart/postfile.pipe.ts @@ -1,12 +1,12 @@ import { Multipart } from '@fastify/multipart'; import { - BadRequestException, Injectable, Logger, PipeTransform, Scope } from '@nestjs/common'; import { FastifyRequest } from 'fastify'; +import { Fail, FT } from 'picsur-shared/dist/types'; import { MultipartConfigService } from '../../config/early/multipart.config.service'; @Injectable({ scope: Scope.REQUEST }) @@ -18,7 +18,7 @@ export class PostFilePipe implements PipeTransform { ) {} async transform({ req }: { req: FastifyRequest }) { - if (!req.isMultipart()) throw new BadRequestException('Invalid file'); + if (!req.isMultipart()) throw Fail(FT.UsrValidation, 'Invalid file'); // Only one file is allowed const file = await req.file({ @@ -27,7 +27,7 @@ export class PostFilePipe implements PipeTransform { files: 1, }, }); - if (file === undefined) throw new BadRequestException('Invalid file'); + if (file === undefined) throw Fail(FT.UsrValidation, 'Invalid file'); // Remove empty fields const allFields: Multipart[] = Object.values(file.fields).filter( @@ -37,14 +37,14 @@ export class PostFilePipe implements PipeTransform { // Remove non-file fields const files = allFields.filter((entry) => entry.file !== undefined); - if (files.length !== 1) throw new BadRequestException('Invalid file'); + if (files.length !== 1) throw Fail(FT.UsrValidation, 'Invalid file'); // Return a buffer of the file try { return await files[0].toBuffer(); } catch (e) { this.logger.warn(e); - throw new BadRequestException('Invalid file'); + throw Fail(FT.Internal, 'Invalid file'); } } } diff --git a/backend/src/layers/exception/exception.filter.ts b/backend/src/layers/exception/exception.filter.ts index d9cf200..ebb219c 100644 --- a/backend/src/layers/exception/exception.filter.ts +++ b/backend/src/layers/exception/exception.filter.ts @@ -25,23 +25,27 @@ export class MainExceptionFilter implements ExceptionFilter { return; } + const status = exception.getCode(); + const type = exception.getType(); + + const message = exception.getReason(); + const logmessage = + message + + (exception.getDebugMessage() ? ' - ' + exception.getDebugMessage() : ''); + if (exception.isImportant()) { MainExceptionFilter.logger.error( - `${traceString} ${exception.getName()}: ${exception.getReason()}`, + `${traceString} ${exception.getName()}: ${logmessage}`, ); if (exception.getStack()) { MainExceptionFilter.logger.debug(exception.getStack()); } } else { MainExceptionFilter.logger.warn( - `${traceString} ${exception.getName()}: ${exception.getReason()}`, + `${traceString} ${exception.getName()}: ${logmessage}`, ); } - const status = exception.getCode(); - const type = exception.getType(); - const message = exception.getReason(); - const toSend: ApiErrorResponse = { success: false, statusCode: status, diff --git a/backend/src/layers/success/success.interceptor.ts b/backend/src/layers/success/success.interceptor.ts index 1d0ffa3..081c812 100644 --- a/backend/src/layers/success/success.interceptor.ts +++ b/backend/src/layers/success/success.interceptor.ts @@ -1,14 +1,13 @@ import { CallHandler, ExecutionContext, - Injectable, - InternalServerErrorException, - Logger, + Injectable, Logger, NestInterceptor, Optional } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ApiAnySuccessResponse } from 'picsur-shared/dist/dto/api/api.dto'; +import { Fail, FT } from 'picsur-shared/dist/types'; import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto'; import { map, Observable } from 'rxjs'; @@ -55,24 +54,24 @@ export class SuccessInterceptor implements NestInterceptor { ); if (!schemaStatic) { - this.logger.warn( + throw Fail( + FT.Internal, + "Couldn't find schema", `No zodSchema found on handler ${context.getHandler().name}`, ); - throw new InternalServerErrorException("Couldn't find schema"); } let schema = schemaStatic.zodSchema; const parseResult = schema.safeParse(data); if (!parseResult.success) { - this.logger.warn( + throw Fail( + FT.Internal, + 'Server produced invalid response', `Function ${context.getHandler().name} failed validation: ${ parseResult.error }`, ); - throw new InternalServerErrorException( - 'Server produced invalid response', - ); } return parseResult.data; diff --git a/backend/src/layers/validate/zod-validator.pipe.ts b/backend/src/layers/validate/zod-validator.pipe.ts index 87965db..2a818ce 100644 --- a/backend/src/layers/validate/zod-validator.pipe.ts +++ b/backend/src/layers/validate/zod-validator.pipe.ts @@ -5,11 +5,11 @@ import { ArgumentMetadata, - BadRequestException, Injectable, Optional, - PipeTransform, + PipeTransform } from '@nestjs/common'; +import { Fail, FT } from 'picsur-shared/dist/types'; import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto'; export interface ZodValidationPipeOptions { @@ -36,7 +36,11 @@ export class ZodValidationPipe implements PipeTransform { const parseResult = zodSchema.safeParse(value); if (!parseResult.success) { - throw new BadRequestException(); + throw Fail( + FT.UsrValidation, + 'Invalid data', + parseResult.error + ); } return parseResult.data; diff --git a/backend/src/managers/auth/auth.service.ts b/backend/src/managers/auth/auth.service.ts index 212941f..76989dc 100644 --- a/backend/src/managers/auth/auth.service.ts +++ b/backend/src/managers/auth/auth.service.ts @@ -19,13 +19,13 @@ export class AuthManagerService { // in case of any failures const result = JwtDataSchema.safeParse(jwtData); if (!result.success) { - return Fail(FT.SysValidation, 'Invalid JWT: ' + result.error); + return Fail(FT.SysValidation, undefined, 'Invalid JWT: ' + result.error); } try { return await this.jwtService.signAsync(result.data); } catch (e) { - return Fail(FT.Internal, "Couldn't create JWT: " + e); + return Fail(FT.Internal, undefined, "Couldn't create JWT: " + e); } } } diff --git a/backend/src/managers/auth/guards/local-auth.strategy.ts b/backend/src/managers/auth/guards/local-auth.strategy.ts index 209be7d..e758434 100644 --- a/backend/src/managers/auth/guards/local-auth.strategy.ts +++ b/backend/src/managers/auth/guards/local-auth.strategy.ts @@ -1,4 +1,4 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-local'; import { EUser } from 'picsur-shared/dist/entities/user.entity'; @@ -15,9 +15,7 @@ export class LocalAuthStrategy extends PassportStrategy(Strategy, 'local') { async validate(username: string, password: string): AsyncFailable { // All this does is call the usersservice authenticate for authentication const user = await this.usersService.authenticate(username, password); - if (HasFailed(user)) { - throw new UnauthorizedException(); - } + if (HasFailed(user)) throw user; return EUserBackend2EUser(user); } diff --git a/backend/src/managers/auth/guards/main.guard.ts b/backend/src/managers/auth/guards/main.guard.ts index 915bc62..dec94f5 100644 --- a/backend/src/managers/auth/guards/main.guard.ts +++ b/backend/src/managers/auth/guards/main.guard.ts @@ -1,8 +1,4 @@ -import { - ExecutionContext, Injectable, - InternalServerErrorException, - Logger -} from '@nestjs/common'; +import { ExecutionContext, Injectable, Logger } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { EUser, EUserSchema } from 'picsur-shared/dist/entities/user.entity'; @@ -30,34 +26,42 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) { // Sanity check const result = await super.canActivate(context); if (result !== true) { - this.logger.error('Main Auth has denied access, this should not happen'); - throw new InternalServerErrorException(); + throw Fail( + FT.Internal, + undefined, + 'Main Auth has denied access, this should not happen', + ); } const user = await this.validateUser( context.switchToHttp().getRequest().user, ); if (!user.id) { - this.logger.error('User has no id, this should not happen'); - throw new InternalServerErrorException(); + throw Fail( + FT.Internal, + undefined, + 'User has no id, this should not happen', + ); } // These are the permissions required to access the route const permissions = this.extractPermissions(context); if (HasFailed(permissions)) { - this.logger.error( + throw Fail( + FT.Internal, + undefined, 'Fetching route permission failed: ' + permissions.getReason(), ); - throw new InternalServerErrorException(); } // These are the permissions the user has const userPermissions = await this.usersService.getPermissions(user.id); if (HasFailed(userPermissions)) { - this.logger.warn( + throw Fail( + FT.Internal, + undefined, 'Fetching user permissions failed: ' + userPermissions.getReason(), ); - throw new InternalServerErrorException(); } context.switchToHttp().getRequest().userPermissions = userPermissions; @@ -78,12 +82,14 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) { if (permissions === undefined) return Fail( FT.Internal, + undefined, `${handlerName} does not have any permissions defined, denying access`, ); if (!isPermissionsArray(permissions)) return Fail( FT.Internal, + undefined, `Permissions for ${handlerName} is not a string array`, ); @@ -93,10 +99,11 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) { private async validateUser(user: EUser): Promise { const result = EUserSchema.safeParse(user); if (!result.success) { - this.logger.warn( + throw Fail( + FT.Internal, + undefined, `Invalid user object, where it should always be valid: ${result.error}`, ); - throw new InternalServerErrorException(); } return result.data; diff --git a/backend/src/routes/api/experiment/experiment.controller.ts b/backend/src/routes/api/experiment/experiment.controller.ts index 2aa34e7..6761052 100644 --- a/backend/src/routes/api/experiment/experiment.controller.ts +++ b/backend/src/routes/api/experiment/experiment.controller.ts @@ -1,13 +1,14 @@ import { Controller, Get, Request } from '@nestjs/common'; import { UserInfoResponse } from 'picsur-shared/dist/dto/api/user-manage.dto'; import { Permission } from 'picsur-shared/dist/dto/permissions.enum'; -import { RequiredPermissions } from '../../../decorators/permissions.decorator'; +import { Fail, FT } from 'picsur-shared/dist/types'; +import { NoPermissions, RequiredPermissions } from '../../../decorators/permissions.decorator'; import { ReqUserID } from '../../../decorators/request-user.decorator'; import { Returns } from '../../../decorators/returns.decorator'; import type AuthFasityRequest from '../../../models/interfaces/authrequest.dto'; @Controller('api/experiment') -//@NoPermissions() +@NoPermissions() @RequiredPermissions(Permission.Settings) export class ExperimentController { @Get() @@ -16,6 +17,7 @@ export class ExperimentController { @Request() req: AuthFasityRequest, @ReqUserID() thing: string, ): Promise { + throw Fail(FT.NotFound, new Error("hello")); return req.user; } } diff --git a/frontend/src/app/services/api/api.service.ts b/frontend/src/app/services/api/api.service.ts index f110a25..808f861 100644 --- a/frontend/src/app/services/api/api.service.ts +++ b/frontend/src/app/services/api/api.service.ts @@ -58,8 +58,11 @@ export class ApiService { const validateResult = sendSchema.safeParse(data); if (!validateResult.success) { - this.logger.error(validateResult.error); - return Fail(FT.SysValidation, 'Something went wrong'); + return Fail( + FT.SysValidation, + 'Something went wrong', + validateResult.error, + ); } return this.fetchSafeJson(receiveType, url, { @@ -92,8 +95,11 @@ export class ApiService { const validateResult = resultSchema.safeParse(result); if (!validateResult.success) { - this.logger.error(validateResult.error); - return Fail(FT.SysValidation, 'Something went wrong'); + return Fail( + FT.SysValidation, + 'Something went wrong', + validateResult.error, + ); } if (validateResult.data.success === false) @@ -113,8 +119,7 @@ export class ApiService { try { return await response.json(); } catch (e) { - this.logger.error(e); - return Fail(FT.Internal, 'Something went wrong'); + return Fail(FT.Internal, e); } } @@ -150,8 +155,7 @@ export class ApiService { name, }; } catch (e) { - this.logger.error(e); - return Fail(FT.Internal, 'Something went wrong'); + return Fail(FT.Internal, e); } } @@ -187,7 +191,7 @@ export class ApiService { error: e, url, }); - return Fail(FT.Network, 'Network Error'); + return Fail(FT.Network, e); } } } diff --git a/shared/src/types/failable.ts b/shared/src/types/failable.ts index cdce68f..9dd590d 100644 --- a/shared/src/types/failable.ts +++ b/shared/src/types/failable.ts @@ -10,7 +10,7 @@ export enum FT { SysValidation = 'sysvalidation', UsrValidation = 'usrvalidation', Permission = 'permission', - NotFound = 'notFound', + NotFound = 'notfound', Conflict = 'conflict', Internal = 'internal', Authentication = 'authentication', @@ -21,6 +21,7 @@ export enum FT { interface FTProp { important: boolean; code: number; + message: string; } const FTProps: { @@ -29,56 +30,68 @@ const FTProps: { [FT.Unknown]: { important: false, code: 500, + message: 'An unkown error occurred', }, [FT.Internal]: { important: true, code: 500, + message: 'An internal error occurred', }, [FT.Database]: { important: true, code: 500, + message: 'A database error occurred', }, [FT.Network]: { important: true, code: 500, + message: 'A network error occurred', }, [FT.SysValidation]: { important: true, code: 500, + message: 'Validation of internal items failed', }, [FT.UsrValidation]: { important: false, code: 400, + message: 'Validation of user input failed', }, [FT.Permission]: { important: false, code: 403, + message: 'Permission denied', }, [FT.NotFound]: { important: false, code: 404, + message: 'Item(s) could not be found', }, [FT.Conflict]: { important: false, code: 409, + message: 'There was a conflict', }, [FT.Authentication]: { important: false, code: 200, - } , + message: 'Authentication failed', + }, [FT.Impossible]: { important: true, code: 422, - } , + message: 'What you are doing is impossible', + }, }; export class Failure { private __68351953531423479708__id_failure = 1148363914; constructor( + private readonly type: FT = FT.Unknown, private readonly reason?: string, private readonly stack?: string, - private readonly type: FT = FT.Unknown, + private readonly debugMessage?: string, ) {} getReason(): string { @@ -89,6 +102,10 @@ export class Failure { return this.stack; } + getDebugMessage(): string | undefined { + return this.debugMessage; + } + getType(): FT { return this.type; } @@ -112,19 +129,34 @@ export class Failure { throw new Error('Invalid failure data'); } - return new Failure(data.reason, data.stack, data.type); + return new Failure(data.type, data.reason, data.stack, data.debugMessage); } } -export function Fail(type: FT, reason: any): Failure { - if (typeof reason === 'string') { - return new Failure(reason, undefined, type); - } else if (reason instanceof Error) { - return new Failure(reason.message, reason.stack, type); - } else if (reason instanceof Failure) { +export function Fail(type: FT, reason?: any, dbgReason?: any): Failure { + const strReason = reason.toString(); + + if (typeof dbgReason === 'string') { + return new Failure(type, strReason, undefined, dbgReason); + } else if (dbgReason instanceof Error) { + return new Failure(type, strReason, dbgReason.stack, dbgReason.message); + } else if (dbgReason instanceof Failure) { throw new Error('Cannot fail with a failure, just return it'); } else { - return new Failure('Unkown reason', undefined, type); + if (typeof reason === 'string') { + return new Failure(type, strReason, undefined, undefined); + } else if (reason instanceof Error) { + return new Failure( + type, + FTProps[type].message, + reason.stack, + reason.message, + ); + } else if (dbgReason instanceof Failure) { + throw new Error('Cannot fail with a failure, just return it'); + } else { + return new Failure(type, FTProps[type].message, undefined, undefined); + } } } diff --git a/shared/src/util/parse-mime.ts b/shared/src/util/parse-mime.ts index 3d20068..f4e1296 100644 --- a/shared/src/util/parse-mime.ts +++ b/shared/src/util/parse-mime.ts @@ -13,5 +13,5 @@ export function ParseMime(mime: string): Failable { if (SupportedAnimMimes.includes(mime)) return { mime, type: SupportedMimeCategory.Animation }; - return Fail(FT.Validation, 'Unsupported mime type'); + return Fail(FT.UsrValidation, 'Unsupported mime type'); }