From ab600c20b786418a3151b4d39c3894a75fb321f8 Mon Sep 17 00:00:00 2001 From: rubikscraft Date: Fri, 26 Aug 2022 20:40:16 +0200 Subject: [PATCH] change error behaviour --- backend/src/app.module.ts | 4 +- .../preference-common.service.ts | 4 +- backend/src/config/late/jwt.config.service.ts | 18 +++--- .../src/decorators/permissions.decorator.ts | 7 ++- .../src/decorators/request-user.decorator.ts | 3 +- .../src/layers/exception/exception.filter.ts | 12 +++- .../src/layers/success/success.interceptor.ts | 14 +++-- .../routes/api/pref/sys-pref.controller.ts | 26 +++----- .../routes/api/pref/usr-pref.controller.ts | 30 ++++----- .../src/routes/api/roles/roles.controller.ts | 57 +++++------------ .../routes/api/user/user-manage.controller.ts | 62 ++++++------------- .../src/routes/api/user/user.controller.ts | 40 +++--------- .../routes/image/image-manage.controller.ts | 35 +++-------- backend/src/routes/image/image.controller.ts | 55 +++++----------- shared/src/dto/api/api.dto.ts | 3 +- shared/src/types/failable.ts | 53 +++++++++++----- 16 files changed, 166 insertions(+), 257 deletions(-) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index af5337f..304e19b 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -25,7 +25,7 @@ const imageCorsConfig = cors({ maxAge: 30 * 24 * 60 * 60, }); -const imageCorpOverride = ( +const imageCorsOverride = ( req: IncomingMessage, res: ServerResponse, next: Function, @@ -54,6 +54,6 @@ const imageCorpOverride = ( export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(mainCorsConfig).exclude('/i').forRoutes('/'); - consumer.apply(imageCorsConfig, imageCorpOverride).forRoutes('/i'); + consumer.apply(imageCorsConfig, imageCorsOverride).forRoutes('/i'); } } diff --git a/backend/src/collections/preference-db/preference-common.service.ts b/backend/src/collections/preference-db/preference-common.service.ts index a2e8f56..2ccc724 100644 --- a/backend/src/collections/preference-db/preference-common.service.ts +++ b/backend/src/collections/preference-db/preference-common.service.ts @@ -56,7 +56,7 @@ export class PreferenceCommonService { case 'boolean': return { key: preference.key, - value: preference.value == 'true', + value: preference.value === 'true', type: 'boolean', }; } @@ -100,7 +100,7 @@ export class PreferenceCommonService { expectedType: PrefValueTypeStrings, ): Failable { const type = typeof value; - if (type != expectedType) { + if (type !== expectedType) { return Fail(FT.UsrValidation, 'Invalid preference value'); } diff --git a/backend/src/config/late/jwt.config.service.ts b/backend/src/config/late/jwt.config.service.ts index 055bf66..e97adba 100644 --- a/backend/src/config/late/jwt.config.service.ts +++ b/backend/src/config/late/jwt.config.service.ts @@ -1,6 +1,6 @@ import { FactoryProvider, Injectable, Logger } from '@nestjs/common'; import { JwtModuleOptions, JwtOptionsFactory } from '@nestjs/jwt'; -import { HasFailed } from 'picsur-shared/dist/types'; +import { ThrowIfFailed } from 'picsur-shared/dist/types'; import { SysPreferenceService } from '../../collections/preference-db/sys-preference-db.service'; @Injectable() @@ -19,20 +19,18 @@ export class JwtConfigService implements JwtOptionsFactory { } public async getJwtSecret(): Promise { - const secret = await this.prefService.getStringPreference('jwt_secret'); - if (HasFailed(secret)) { - throw new Error('JWT secret could not be retrieved'); - } + const secret = ThrowIfFailed( + await this.prefService.getStringPreference('jwt_secret'), + ); + return secret; } public async getJwtExpiresIn(): Promise { - const expiresIn = await this.prefService.getStringPreference( - 'jwt_expires_in', + const expiresIn = ThrowIfFailed( + await this.prefService.getStringPreference('jwt_expires_in'), ); - if (HasFailed(expiresIn)) { - throw new Error('JWT expiresIn could not be retrieved'); - } + return expiresIn; } diff --git a/backend/src/decorators/permissions.decorator.ts b/backend/src/decorators/permissions.decorator.ts index e2e0be1..2719d1f 100644 --- a/backend/src/decorators/permissions.decorator.ts +++ b/backend/src/decorators/permissions.decorator.ts @@ -2,8 +2,9 @@ import { createParamDecorator, ExecutionContext, SetMetadata, - UseGuards, + UseGuards } from '@nestjs/common'; +import { Fail, FT } from 'picsur-shared/dist/types'; import { CombineFCDecorators } from 'picsur-shared/dist/util/decorator'; import { LocalAuthGuard } from '../managers/auth/guards/local-auth.guard'; import { Permission, Permissions } from '../models/constants/permissions.const'; @@ -28,7 +29,7 @@ export const HasPermission = createParamDecorator( const req: AuthFasityRequest = ctx.switchToHttp().getRequest(); const permissions = req.userPermissions; if (!permissions) { - throw new Error('Permissions are missing from request'); + throw Fail(FT.Internal, undefined, 'Permissions are missing from request'); } return permissions.includes(data); @@ -40,7 +41,7 @@ export const GetPermissions = createParamDecorator( const req: AuthFasityRequest = ctx.switchToHttp().getRequest(); const permissions = req.userPermissions; if (!permissions) { - throw new Error('Permissions are missing from request'); + throw Fail(FT.Internal, undefined, 'Permissions are missing from request'); } return permissions; diff --git a/backend/src/decorators/request-user.decorator.ts b/backend/src/decorators/request-user.decorator.ts index 3126971..4ff0dfc 100644 --- a/backend/src/decorators/request-user.decorator.ts +++ b/backend/src/decorators/request-user.decorator.ts @@ -1,4 +1,5 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { Fail, FT } from 'picsur-shared/dist/types'; import AuthFasityRequest from '../models/interfaces/authrequest.dto'; export const ReqUser = createParamDecorator( @@ -12,7 +13,7 @@ export const ReqUserID = createParamDecorator( (input: any, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest() as AuthFasityRequest; const id = request.user.id; - if (!id) throw new Error('User ID is not set'); + if (!id) throw Fail(FT.Internal, undefined, 'User ID is not set'); return id; }, ); diff --git a/backend/src/layers/exception/exception.filter.ts b/backend/src/layers/exception/exception.filter.ts index ebb219c..1691eb1 100644 --- a/backend/src/layers/exception/exception.filter.ts +++ b/backend/src/layers/exception/exception.filter.ts @@ -1,7 +1,12 @@ import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common'; import { FastifyReply, FastifyRequest } from 'fastify'; import { ApiErrorResponse } from 'picsur-shared/dist/dto/api/api.dto'; -import { IsFailure } from 'picsur-shared/dist/types/failable'; +import { + Fail, + Failure, + FT, + IsFailure +} from 'picsur-shared/dist/types/failable'; // This will catch any exception that is made in any request // (As long as its within nest, the earlier fastify stages are not handled here) @@ -11,7 +16,7 @@ import { IsFailure } from 'picsur-shared/dist/types/failable'; export class MainExceptionFilter implements ExceptionFilter { private static readonly logger = new Logger('MainExceptionFilter'); - catch(exception: unknown, host: ArgumentsHost) { + catch(exception: Failure, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); @@ -22,7 +27,7 @@ export class MainExceptionFilter implements ExceptionFilter { MainExceptionFilter.logger.error( traceString + ' Unkown exception: ' + exception, ); - return; + exception = Fail(FT.Internal, 'Unknown exception', exception); } const status = exception.getCode(); @@ -50,6 +55,7 @@ export class MainExceptionFilter implements ExceptionFilter { success: false, statusCode: status, timestamp: new Date().toISOString(), + timeMs: Math.round(response.getResponseTime()), data: { type, diff --git a/backend/src/layers/success/success.interceptor.ts b/backend/src/layers/success/success.interceptor.ts index 081c812..a18bebd 100644 --- a/backend/src/layers/success/success.interceptor.ts +++ b/backend/src/layers/success/success.interceptor.ts @@ -1,11 +1,13 @@ import { CallHandler, ExecutionContext, - Injectable, Logger, + Injectable, + Logger, NestInterceptor, Optional } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; +import { FastifyReply } from 'fastify'; 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'; @@ -81,15 +83,17 @@ export class SuccessInterceptor implements NestInterceptor { context: ExecutionContext, data: unknown, ): ApiAnySuccessResponse { - const status = context.switchToHttp().getResponse().statusCode; - const response = { + const response = context.switchToHttp().getResponse(); + + const newResponse: ApiAnySuccessResponse = { success: true as true, // really typescript - statusCode: status, + statusCode: response.statusCode, timestamp: new Date().toISOString(), + timeMs: Math.round(response.getResponseTime()), data, }; - return response; + return newResponse; } } diff --git a/backend/src/routes/api/pref/sys-pref.controller.ts b/backend/src/routes/api/pref/sys-pref.controller.ts index c261754..a4b4239 100644 --- a/backend/src/routes/api/pref/sys-pref.controller.ts +++ b/backend/src/routes/api/pref/sys-pref.controller.ts @@ -1,9 +1,7 @@ import { Body, Controller, - Get, - InternalServerErrorException, - Logger, + Get, Logger, Param, Post } from '@nestjs/common'; @@ -13,7 +11,7 @@ import { UpdatePreferenceRequest, UpdatePreferenceResponse } from 'picsur-shared/dist/dto/api/pref.dto'; -import { HasFailed } from 'picsur-shared/dist/types'; +import { ThrowIfFailed } from 'picsur-shared/dist/types'; import { SysPreferenceService } from '../../../collections/preference-db/sys-preference-db.service'; import { RequiredPermissions } from '../../../decorators/permissions.decorator'; import { Returns } from '../../../decorators/returns.decorator'; @@ -29,11 +27,7 @@ export class SysPrefController { @Get() @Returns(MultiplePreferencesResponse) async getAllSysPrefs(): Promise { - const prefs = await this.prefService.getAllPreferences(); - if (HasFailed(prefs)) { - this.logger.warn(prefs.getReason()); - throw new InternalServerErrorException('Could not get preferences'); - } + const prefs = ThrowIfFailed(await this.prefService.getAllPreferences()); return { results: prefs, @@ -44,11 +38,7 @@ export class SysPrefController { @Get(':key') @Returns(GetPreferenceResponse) async getSysPref(@Param('key') key: string): Promise { - const pref = await this.prefService.getPreference(key); - if (HasFailed(pref)) { - this.logger.warn(pref.getReason()); - throw new InternalServerErrorException('Could not get preference'); - } + const pref = ThrowIfFailed(await this.prefService.getPreference(key)); return pref; } @@ -61,11 +51,9 @@ export class SysPrefController { ): Promise { const value = body.value; - const pref = await this.prefService.setPreference(key, value); - if (HasFailed(pref)) { - this.logger.warn(pref.getReason()); - throw new InternalServerErrorException('Could not set preference'); - } + const pref = ThrowIfFailed( + await this.prefService.setPreference(key, value), + ); return { key, diff --git a/backend/src/routes/api/pref/usr-pref.controller.ts b/backend/src/routes/api/pref/usr-pref.controller.ts index 8ffa551..4faf894 100644 --- a/backend/src/routes/api/pref/usr-pref.controller.ts +++ b/backend/src/routes/api/pref/usr-pref.controller.ts @@ -1,9 +1,7 @@ import { Body, Controller, - Get, - InternalServerErrorException, - Logger, + Get, Logger, Param, Post } from '@nestjs/common'; @@ -13,7 +11,7 @@ import { UpdatePreferenceRequest, UpdatePreferenceResponse } from 'picsur-shared/dist/dto/api/pref.dto'; -import { HasFailed } from 'picsur-shared/dist/types'; +import { ThrowIfFailed } from 'picsur-shared/dist/types'; import { UsrPreferenceService } from '../../../collections/preference-db/usr-preference-db.service'; import { RequiredPermissions } from '../../../decorators/permissions.decorator'; import { ReqUserID } from '../../../decorators/request-user.decorator'; @@ -32,11 +30,9 @@ export class UsrPrefController { async getAllSysPrefs( @ReqUserID() userid: string, ): Promise { - const prefs = await this.prefService.getAllPreferences(userid); - if (HasFailed(prefs)) { - this.logger.warn(prefs.getReason()); - throw new InternalServerErrorException('Could not get preferences'); - } + const prefs = ThrowIfFailed( + await this.prefService.getAllPreferences(userid), + ); return { results: prefs, @@ -50,11 +46,9 @@ export class UsrPrefController { @Param('key') key: string, @ReqUserID() userid: string, ): Promise { - const pref = await this.prefService.getPreference(userid, key); - if (HasFailed(pref)) { - this.logger.warn(pref.getReason()); - throw new InternalServerErrorException('Could not get preference'); - } + const pref = ThrowIfFailed( + await this.prefService.getPreference(userid, key), + ); return pref; } @@ -68,11 +62,9 @@ export class UsrPrefController { ): Promise { const value = body.value; - const pref = await this.prefService.setPreference(userid, key, value); - if (HasFailed(pref)) { - this.logger.warn(pref.getReason()); - throw new InternalServerErrorException('Could not set preference'); - } + const pref = ThrowIfFailed( + await this.prefService.setPreference(userid, key, value), + ); return { key, diff --git a/backend/src/routes/api/roles/roles.controller.ts b/backend/src/routes/api/roles/roles.controller.ts index 5217f1f..67b52bd 100644 --- a/backend/src/routes/api/roles/roles.controller.ts +++ b/backend/src/routes/api/roles/roles.controller.ts @@ -1,9 +1,7 @@ import { Body, Controller, - Get, - InternalServerErrorException, - Logger, + Get, Logger, Post } from '@nestjs/common'; import { @@ -18,7 +16,7 @@ import { RoleUpdateResponse, SpecialRolesResponse } from 'picsur-shared/dist/dto/api/roles.dto'; -import { HasFailed } from 'picsur-shared/dist/types'; +import { Fail, FT, ThrowIfFailed } from 'picsur-shared/dist/types'; import { RolesService } from '../../../collections/role-db/role-db.service'; import { UsersService } from '../../../collections/user-db/user-db.service'; import { RequiredPermissions } from '../../../decorators/permissions.decorator'; @@ -45,11 +43,7 @@ export class RolesController { @Get('list') @Returns(RoleListResponse) async getRoles(): Promise { - const roles = await this.rolesService.findAll(); - if (HasFailed(roles)) { - this.logger.warn(roles.getReason()); - throw new InternalServerErrorException('Could not list roles'); - } + const roles = ThrowIfFailed(await this.rolesService.findAll()); return { results: roles, @@ -60,11 +54,7 @@ export class RolesController { @Post('info') @Returns(RoleInfoResponse) async getRole(@Body() body: RoleInfoRequest): Promise { - const role = await this.rolesService.findOne(body.name); - if (HasFailed(role)) { - this.logger.warn(role.getReason()); - throw new InternalServerErrorException('Could not find role'); - } + const role = ThrowIfFailed(await this.rolesService.findOne(body.name)); return role; } @@ -76,17 +66,12 @@ export class RolesController { ): Promise { const permissions = body.permissions; if (!isPermissionsArray(permissions)) { - throw new InternalServerErrorException('Invalid permissions'); + throw Fail(FT.UsrValidation, 'Invalid permissions array'); } - const updatedRole = await this.rolesService.setPermissions( - body.name, - permissions, + const updatedRole = ThrowIfFailed( + await this.rolesService.setPermissions(body.name, permissions), ); - if (HasFailed(updatedRole)) { - this.logger.warn(updatedRole.getReason()); - throw new InternalServerErrorException('Could not set role permissions'); - } return updatedRole; } @@ -98,14 +83,12 @@ export class RolesController { ): Promise { const permissions = role.permissions; if (!isPermissionsArray(permissions)) { - throw new InternalServerErrorException('Invalid permissions array'); + throw Fail(FT.UsrValidation, 'Invalid permissions array'); } - const newRole = await this.rolesService.create(role.name, permissions); - if (HasFailed(newRole)) { - this.logger.warn(newRole.getReason()); - throw new InternalServerErrorException('Could not create role'); - } + const newRole = ThrowIfFailed( + await this.rolesService.create(role.name, permissions), + ); return newRole; } @@ -115,19 +98,13 @@ export class RolesController { async deleteRole( @Body() role: RoleDeleteRequest, ): Promise { - const deletedRole = await this.rolesService.delete(role.name); - if (HasFailed(deletedRole)) { - this.logger.warn(deletedRole.getReason()); - throw new InternalServerErrorException('Could not delete role'); - } + const deletedRole = ThrowIfFailed( + await this.rolesService.delete(role.name), + ); - const success = await this.usersService.removeRoleEveryone(role.name); - if (HasFailed(success)) { - this.logger.warn(success.getReason()); - throw new InternalServerErrorException( - 'Could not remove role from users', - ); - } + ThrowIfFailed( + await this.usersService.removeRoleEveryone(role.name), + ); return deletedRole; } diff --git a/backend/src/routes/api/user/user-manage.controller.ts b/backend/src/routes/api/user/user-manage.controller.ts index 17a6652..d448952 100644 --- a/backend/src/routes/api/user/user-manage.controller.ts +++ b/backend/src/routes/api/user/user-manage.controller.ts @@ -1,9 +1,7 @@ import { Body, Controller, - Get, - InternalServerErrorException, - Logger, + Get, Logger, Post } from '@nestjs/common'; import { @@ -19,7 +17,7 @@ import { UserUpdateRequest, UserUpdateResponse } from 'picsur-shared/dist/dto/api/user-manage.dto'; -import { HasFailed } from 'picsur-shared/dist/types'; +import { ThrowIfFailed } from 'picsur-shared/dist/types'; import { UsersService } from '../../../collections/user-db/user-db.service'; import { RequiredPermissions } from '../../../decorators/permissions.decorator'; import { Returns } from '../../../decorators/returns.decorator'; @@ -43,11 +41,9 @@ export class UserAdminController { async listUsersPaged( @Body() body: UserListRequest, ): Promise { - const found = await this.usersService.findMany(body.count, body.page); - if (HasFailed(found)) { - this.logger.warn(found.getReason()); - throw new InternalServerErrorException('Could not list users'); - } + const found = ThrowIfFailed( + await this.usersService.findMany(body.count, body.page), + ); found.results = found.results.map(EUserBackend2EUser); return found; @@ -58,15 +54,13 @@ export class UserAdminController { async register( @Body() create: UserCreateRequest, ): Promise { - const user = await this.usersService.create( - create.username, - create.password, - create.roles, + const user = ThrowIfFailed( + await this.usersService.create( + create.username, + create.password, + create.roles, + ), ); - if (HasFailed(user)) { - this.logger.warn(user.getReason()); - throw new InternalServerErrorException('Could not create user'); - } return EUserBackend2EUser(user); } @@ -74,11 +68,7 @@ export class UserAdminController { @Post('delete') @Returns(UserDeleteResponse) async delete(@Body() body: UserDeleteRequest): Promise { - const user = await this.usersService.delete(body.id); - if (HasFailed(user)) { - this.logger.warn(user.getReason()); - throw new InternalServerErrorException('Could not delete user'); - } + const user = ThrowIfFailed(await this.usersService.delete(body.id)); return EUserBackend2EUser(user); } @@ -86,11 +76,7 @@ export class UserAdminController { @Post('info') @Returns(UserInfoResponse) async getUser(@Body() body: UserInfoRequest): Promise { - const user = await this.usersService.findOne(body.id); - if (HasFailed(user)) { - this.logger.warn(user.getReason()); - throw new InternalServerErrorException('Could not find user'); - } + const user = ThrowIfFailed(await this.usersService.findOne(body.id)); return EUserBackend2EUser(user); } @@ -100,26 +86,18 @@ export class UserAdminController { async setPermissions( @Body() body: UserUpdateRequest, ): Promise { - let user = await this.usersService.findOne(body.id); - if (HasFailed(user)) { - this.logger.warn(user.getReason()); - throw new InternalServerErrorException('Could not find user'); - } + let user = ThrowIfFailed(await this.usersService.findOne(body.id)); if (body.roles) { - user = await this.usersService.setRoles(body.id, body.roles); - if (HasFailed(user)) { - this.logger.warn(user.getReason()); - throw new InternalServerErrorException('Could not update user'); - } + user = ThrowIfFailed( + await this.usersService.setRoles(body.id, body.roles), + ); } if (body.password) { - user = await this.usersService.updatePassword(body.id, body.password); - if (HasFailed(user)) { - this.logger.warn(user.getReason()); - throw new InternalServerErrorException('Could not update user'); - } + user = ThrowIfFailed( + await this.usersService.updatePassword(body.id, body.password), + ); } return EUserBackend2EUser(user); diff --git a/backend/src/routes/api/user/user.controller.ts b/backend/src/routes/api/user/user.controller.ts index eef6ad5..7230a03 100644 --- a/backend/src/routes/api/user/user.controller.ts +++ b/backend/src/routes/api/user/user.controller.ts @@ -1,9 +1,7 @@ import { Body, Controller, - Get, - InternalServerErrorException, - Logger, + Get, Logger, Post } from '@nestjs/common'; import { @@ -14,7 +12,7 @@ import { UserRegisterResponse } from 'picsur-shared/dist/dto/api/user.dto'; import type { EUser } from 'picsur-shared/dist/entities/user.entity'; -import { HasFailed } from 'picsur-shared/dist/types'; +import { ThrowIfFailed } from 'picsur-shared/dist/types'; import { UsersService } from '../../../collections/user-db/user-db.service'; import { NoPermissions, @@ -40,11 +38,7 @@ export class UserController { @Returns(UserLoginResponse) @UseLocalAuth(Permission.UserLogin) async login(@ReqUser() user: EUser): Promise { - const jwt_token = await this.authService.createToken(user); - if (HasFailed(jwt_token)) { - this.logger.warn(jwt_token.getReason()); - throw new InternalServerErrorException('Could not get new token'); - } + const jwt_token = ThrowIfFailed(await this.authService.createToken(user)); return { jwt_token }; } @@ -55,14 +49,9 @@ export class UserController { async register( @Body() register: UserRegisterRequest, ): Promise { - const user = await this.usersService.create( - register.username, - register.password, + const user = ThrowIfFailed( + await this.usersService.create(register.username, register.password), ); - if (HasFailed(user)) { - this.logger.warn(user.getReason()); - throw new InternalServerErrorException('Could not register user'); - } return EUserBackend2EUser(user); } @@ -71,20 +60,11 @@ export class UserController { @Returns(UserMeResponse) @RequiredPermissions(Permission.UserKeepLogin) async me(@ReqUserID() userid: string): Promise { - const backenduser = await this.usersService.findOne(userid); - - if (HasFailed(backenduser)) { - this.logger.warn(backenduser.getReason()); - throw new InternalServerErrorException('Could not get user'); - } + const backenduser = ThrowIfFailed(await this.usersService.findOne(userid)); const user = EUserBackend2EUser(backenduser); - const token = await this.authService.createToken(user); - if (HasFailed(token)) { - this.logger.warn(token.getReason()); - throw new InternalServerErrorException('Could not get new token'); - } + const token = ThrowIfFailed(await this.authService.createToken(user)); return { user, token }; } @@ -96,11 +76,7 @@ export class UserController { async refresh( @ReqUserID() userid: string, ): Promise { - const permissions = await this.usersService.getPermissions(userid); - if (HasFailed(permissions)) { - this.logger.warn(permissions.getReason()); - throw new InternalServerErrorException('Could not get permissions'); - } + const permissions = ThrowIfFailed(await this.usersService.getPermissions(userid)); return { permissions }; } diff --git a/backend/src/routes/image/image-manage.controller.ts b/backend/src/routes/image/image-manage.controller.ts index e6c5a2b..8430792 100644 --- a/backend/src/routes/image/image-manage.controller.ts +++ b/backend/src/routes/image/image-manage.controller.ts @@ -1,8 +1,6 @@ import { - BadRequestException, Body, Controller, - InternalServerErrorException, Logger, Post } from '@nestjs/common'; @@ -14,7 +12,7 @@ import { ImageUploadResponse } from 'picsur-shared/dist/dto/api/image-manage.dto'; import { Permission } from 'picsur-shared/dist/dto/permissions.enum'; -import { HasFailed } from 'picsur-shared/dist/types'; +import { ThrowIfFailed } from 'picsur-shared/dist/types'; import { MultiPart } from '../../decorators/multipart/multipart.decorator'; import { HasPermission, @@ -38,14 +36,9 @@ export class ImageManageController { @MultiPart() multipart: ImageUploadDto, @ReqUserID() userid: string, ): Promise { - const image = await this.imagesService.upload( - multipart.image.buffer, - userid, + const image = ThrowIfFailed( + await this.imagesService.upload(multipart.image.buffer, userid), ); - if (HasFailed(image)) { - this.logger.warn(image.getReason(), image.getStack()); - throw new InternalServerErrorException('Could not upload image'); - } return image; } @@ -61,15 +54,9 @@ export class ImageManageController { body.user_id = userid; } - const found = await this.imagesService.findMany( - body.count, - body.page, - body.user_id, + const found = ThrowIfFailed( + await this.imagesService.findMany(body.count, body.page, body.user_id), ); - if (HasFailed(found)) { - this.logger.warn(found.getReason()); - throw new InternalServerErrorException('Could not list images'); - } return found; } @@ -81,14 +68,12 @@ export class ImageManageController { @ReqUserID() userid: string, @HasPermission(Permission.ImageAdmin) isImageAdmin: boolean, ): Promise { - const deletedImages = await this.imagesService.deleteMany( - body.ids, - isImageAdmin ? undefined : userid, + const deletedImages = ThrowIfFailed( + await this.imagesService.deleteMany( + body.ids, + isImageAdmin ? undefined : userid, + ), ); - if (HasFailed(deletedImages)) { - this.logger.warn(deletedImages.getReason()); - throw new BadRequestException('Could not delete images'); - } return { images: deletedImages, diff --git a/backend/src/routes/image/image.controller.ts b/backend/src/routes/image/image.controller.ts index f455ca1..c3b12f6 100644 --- a/backend/src/routes/image/image.controller.ts +++ b/backend/src/routes/image/image.controller.ts @@ -1,11 +1,7 @@ import { Controller, Get, - Head, - InternalServerErrorException, - Logger, - NotFoundException, - Query, + Head, Logger, Query, Res } from '@nestjs/common'; import type { FastifyReply } from 'fastify'; @@ -13,7 +9,7 @@ import { ImageMetaResponse, ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto'; -import { HasFailed } from 'picsur-shared/dist/types'; +import { ThrowIfFailed } from 'picsur-shared/dist/types'; import { UsersService } from '../../collections/user-db/user-db.service'; import { ImageFullIdParam } from '../../decorators/image-id/image-full-id.decorator'; import { ImageIdParam } from '../../decorators/image-id/image-id.decorator'; @@ -41,11 +37,9 @@ export class ImageController { @ImageFullIdParam() fullid: ImageFullId, ) { if (fullid.type === 'original') { - const fullmime = await this.imagesService.getOriginalMime(fullid.id); - if (HasFailed(fullmime)) { - this.logger.warn(fullmime.getReason()); - throw new NotFoundException('Could not find image'); - } + const fullmime = ThrowIfFailed( + await this.imagesService.getOriginalMime(fullid.id), + ); res.type(fullmime.mime); return; @@ -63,25 +57,17 @@ export class ImageController { @Query() params: ImageRequestParams, ): Promise { if (fullid.type === 'original') { - const image = await this.imagesService.getOriginal(fullid.id); - if (HasFailed(image)) { - this.logger.warn(image.getReason()); - throw new NotFoundException('Could not find image'); - } + const image = ThrowIfFailed( + await this.imagesService.getOriginal(fullid.id), + ); res.type(image.mime); return image.data; } - const image = await this.imagesService.getConverted( - fullid.id, - fullid.mime, - params, + const image = ThrowIfFailed( + await this.imagesService.getConverted(fullid.id, fullid.mime, params), ); - if (HasFailed(image)) { - this.logger.warn(image.getReason()); - throw new NotFoundException('Failed to get image'); - } res.type(image.mime); return image.data; @@ -90,24 +76,15 @@ export class ImageController { @Get('meta/:id') @Returns(ImageMetaResponse) async getImageMeta(@ImageIdParam() id: string): Promise { - const image = await this.imagesService.findOne(id); - if (HasFailed(image)) { - this.logger.warn(image.getReason()); - throw new NotFoundException('Could not find image'); - } + const image = ThrowIfFailed(await this.imagesService.findOne(id)); - const [fileMimes, imageUser] = await Promise.all([ + const [fileMimesRes, imageUserRes] = await Promise.all([ this.imagesService.getFileMimes(id), this.userService.findOne(image.user_id), - ]); - if (HasFailed(fileMimes)) { - this.logger.warn(fileMimes.getReason()); - throw new InternalServerErrorException('Could not get image mime'); - } - if (HasFailed(imageUser)) { - this.logger.warn(imageUser.getReason()); - throw new InternalServerErrorException('Could not get image user'); - } + ]) + + const fileMimes = ThrowIfFailed(fileMimesRes); + const imageUser = ThrowIfFailed(imageUserRes); return { image, user: EUserBackend2EUser(imageUser), fileMimes }; } diff --git a/shared/src/dto/api/api.dto.ts b/shared/src/dto/api/api.dto.ts index c2ecb38..ae5716d 100644 --- a/shared/src/dto/api/api.dto.ts +++ b/shared/src/dto/api/api.dto.ts @@ -1,8 +1,9 @@ import z from 'zod'; const ApiResponseBase = z.object({ - statusCode: z.number().min(0).max(600), + statusCode: z.number().min(0).max(600).int(), timestamp: z.string(), + timeMs: z.number().min(0).int(), }); const ApiSuccessResponse = (data: T) => diff --git a/shared/src/types/failable.ts b/shared/src/types/failable.ts index 9dd590d..0a80da3 100644 --- a/shared/src/types/failable.ts +++ b/shared/src/types/failable.ts @@ -134,34 +134,51 @@ export class Failure { } export function Fail(type: FT, reason?: any, dbgReason?: any): Failure { - const strReason = reason.toString(); + if (IsFailure(reason) || IsFailure(dbgReason)) { + throw new Error('Cannot fail with another failure, just return it'); + } - 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 { - if (typeof reason === 'string') { - return new Failure(type, strReason, undefined, undefined); + if (dbgReason === undefined || dbgReason === null) { + if (reason === undefined || reason === null) { + // If both are null, just return a default error message + return new Failure(type, FTProps[type].message, undefined, undefined); + } else if (typeof reason === 'string') { + // If it is a string, this was intentionally specified, so pass it through + return new Failure(type, reason, undefined, undefined); } else if (reason instanceof Error) { + // In case of an error, we want to keep that hidden, so return the default message + // Only send the specifics to debug 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); + // No clue what it is, so just transform it to a string and return the default message + return new Failure( + type, + FTProps[type].message, + undefined, + String(reason), + ); + } + } else { + // In this case we only accept strings for the reason + const strReason = reason?.toString() ?? FTProps[type].message; + + 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 { + return new Failure(type, strReason, undefined, String(dbgReason)); } } } export function IsFailure(value: any): value is Failure { - return value.__68351953531423479708__id_failure === 1148363914; + return value?.__68351953531423479708__id_failure === 1148363914; } export type Failable = T | Failure; @@ -178,6 +195,14 @@ export function HasSuccess(failable: Failable): failable is T { return (failable as any).__68351953531423479708__id_failure !== 1148363914; } +export function ThrowIfFailed(failable: Failable): V { + if (HasFailed(failable)) { + throw failable; + } + + return failable; +} + export function Map( failable: Failable, mapper: (value: T) => U,