From c8722d89445b2818200da854d5d56914ffd7e8b2 Mon Sep 17 00:00:00 2001 From: rubikscraft Date: Mon, 4 Jul 2022 17:11:42 +0200 Subject: [PATCH] Change failure behaviour --- .../collections/image-db/image-db.service.ts | 26 ++--- .../image-db/image-file-db.service.ts | 18 +-- .../preference-common.service.ts | 9 +- .../sys-preference-db.service.ts | 14 +-- .../usr-preference-db.service.ts | 15 +-- .../src/collections/role-db/role-db.module.ts | 7 +- .../collections/role-db/role-db.service.ts | 35 +++--- .../collections/user-db/user-db.service.ts | 36 +++--- .../src/layers/exception/exception.filter.ts | 49 ++++---- backend/src/managers/auth/auth.service.ts | 6 +- .../src/managers/auth/guards/main.guard.ts | 14 ++- .../managers/image/image-converter.service.ts | 11 +- .../managers/image/image-processor.service.ts | 8 +- backend/src/managers/image/image.service.ts | 17 ++- backend/src/models/constants/roles.const.ts | 3 + backend/src/models/dto/multipart.dto.ts | 4 +- .../api/experiment/experiment.controller.ts | 1 + backend/src/workers/sharp.wrapper.ts | 11 +- .../src/app/models/forms/login.control.ts | 6 +- .../src/app/models/forms/register.control.ts | 6 +- frontend/src/app/services/api/api.service.ts | 19 +-- .../src/app/services/api/image.service.ts | 5 +- frontend/src/app/services/api/info.service.ts | 7 +- .../src/app/services/api/sys-pref.service.ts | 17 ++- frontend/src/app/services/api/user.service.ts | 8 +- .../src/app/services/api/usr-pref.service.ts | 23 +++- frontend/src/app/workers/qoi.job.ts | 6 +- shared/src/dto/api/api.dto.ts | 1 + shared/src/types/failable.ts | 108 ++++++++++++++++-- shared/src/util/parse-mime.ts | 4 +- 30 files changed, 320 insertions(+), 174 deletions(-) diff --git a/backend/src/collections/image-db/image-db.service.ts b/backend/src/collections/image-db/image-db.service.ts index 3bc3d97..050d195 100644 --- a/backend/src/collections/image-db/image-db.service.ts +++ b/backend/src/collections/image-db/image-db.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { AsyncFailable, Fail } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types'; import { FindResult } from 'picsur-shared/dist/types/find-result'; import { In, Repository } from 'typeorm'; import { EImageDerivativeBackend } from '../../models/entities/image-derivative.entity'; @@ -28,7 +28,7 @@ export class ImageDBService { try { imageEntity = await this.imageRepo.save(imageEntity, { reload: true }); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } return imageEntity; @@ -43,10 +43,10 @@ export class ImageDBService { where: { id, user_id: userid }, }); - if (!found) return Fail('Image not found'); + if (!found) return Fail(FT.NotFound, 'Image not found'); return found; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -55,8 +55,8 @@ export class ImageDBService { page: number, userid: string | undefined, ): AsyncFailable> { - if (count < 1 || page < 0) return Fail('Invalid page'); - if (count > 100) return Fail('Too many results'); + if (count < 1 || page < 0) return Fail(FT.UsrValidation, 'Invalid page'); + if (count > 100) return Fail(FT.UsrValidation, 'Too many results'); try { const [found, amount] = await this.imageRepo.findAndCount({ @@ -67,7 +67,7 @@ export class ImageDBService { }, }); - if (found === undefined) return Fail('Images not found'); + if (found === undefined) return Fail(FT.NotFound, 'Images not found'); return { results: found, @@ -76,7 +76,7 @@ export class ImageDBService { pages: Math.ceil(amount / count), }; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -85,7 +85,7 @@ export class ImageDBService { userid: string | undefined, ): AsyncFailable { if (ids.length === 0) return []; - if (ids.length > 500) return Fail('Too many results'); + if (ids.length > 500) return Fail(FT.UsrValidation, 'Too many results'); try { const deletable_images = await this.imageRepo.find({ @@ -97,7 +97,7 @@ export class ImageDBService { const available_ids = deletable_images.map((i) => i.id); - if (available_ids.length === 0) return Fail('Images not found'); + if (available_ids.length === 0) return Fail(FT.NotFound, 'Images not found'); await Promise.all([ this.imageDerivativeRepo.delete({ @@ -112,20 +112,20 @@ export class ImageDBService { return deletable_images; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } public async deleteAll(IAmSure: boolean): AsyncFailable { if (!IAmSure) - return Fail('You must confirm that you want to delete all images'); + return Fail(FT.SysValidation, 'You must confirm that you want to delete all images'); try { await this.imageDerivativeRepo.delete({}); await this.imageFileRepo.delete({}); await this.imageRepo.delete({}); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } return true; } diff --git a/backend/src/collections/image-db/image-file-db.service.ts b/backend/src/collections/image-db/image-file-db.service.ts index 4ee3cd3..b6afa4d 100644 --- a/backend/src/collections/image-db/image-file-db.service.ts +++ b/backend/src/collections/image-db/image-file-db.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ImageFileType } from 'picsur-shared/dist/dto/image-file-types.enum'; -import { AsyncFailable, Fail } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types'; import { LessThan, Repository } from 'typeorm'; import { EImageDerivativeBackend } from '../../models/entities/image-derivative.entity'; import { EImageFileBackend } from '../../models/entities/image-file.entity'; @@ -35,7 +35,7 @@ export class ImageFileDBService { conflictPaths: ['image_id', 'type'], }); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } return true; @@ -50,10 +50,10 @@ export class ImageFileDBService { where: { image_id: imageId ?? '', type: type ?? '' }, }); - if (!found) return Fail('Image not found'); + if (!found) return Fail(FT.NotFound, 'Image not found'); return found; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -67,7 +67,7 @@ export class ImageFileDBService { select: ['type', 'mime'], }); - if (!found) return Fail('Image not found'); + if (!found) return Fail(FT.NotFound, 'Image not found'); const result: { [key in ImageFileType]?: string } = {}; for (const file of found) { @@ -76,7 +76,7 @@ export class ImageFileDBService { return result; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -96,7 +96,7 @@ export class ImageFileDBService { try { return await this.imageDerivativeRepo.save(imageDerivative); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -120,7 +120,7 @@ export class ImageFileDBService { return derivative; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -134,7 +134,7 @@ export class ImageFileDBService { return result.affected ?? 0; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } } diff --git a/backend/src/collections/preference-db/preference-common.service.ts b/backend/src/collections/preference-db/preference-common.service.ts index 6e4acec..a2e8f56 100644 --- a/backend/src/collections/preference-db/preference-common.service.ts +++ b/backend/src/collections/preference-db/preference-common.service.ts @@ -8,6 +8,7 @@ import { AsyncFailable, Fail, Failable, + FT, HasFailed } from 'picsur-shared/dist/types'; @@ -60,7 +61,7 @@ export class PreferenceCommonService { }; } - return Fail('Invalid preference value'); + return Fail(FT.UsrValidation, 'Invalid preference value'); } public async EncodePref( @@ -88,7 +89,7 @@ export class PreferenceCommonService { ): Failable { const keysList = Object.values(prefType); if (!keysList.includes(key)) { - return Fail('Invalid preference key'); + return Fail(FT.UsrValidation, 'Invalid preference key'); } return key as V; @@ -100,7 +101,7 @@ export class PreferenceCommonService { ): Failable { const type = typeof value; if (type != expectedType) { - return Fail('Invalid preference value'); + return Fail(FT.UsrValidation, 'Invalid preference value'); } switch (type) { @@ -112,6 +113,6 @@ export class PreferenceCommonService { return value ? 'true' : 'false'; } - return Fail('Invalid preference value'); + return Fail(FT.UsrValidation, 'Invalid preference value'); } } 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 efd4f89..6433aec 100644 --- a/backend/src/collections/preference-db/sys-preference-db.service.ts +++ b/backend/src/collections/preference-db/sys-preference-db.service.ts @@ -6,7 +6,7 @@ import { PrefValueTypeStrings } from 'picsur-shared/dist/dto/preferences.dto'; import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum'; -import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types'; import { Repository } from 'typeorm'; import { SysPreferenceList, @@ -46,7 +46,7 @@ export class SysPreferenceService { conflictPaths: ['key'], }); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } return { @@ -74,13 +74,13 @@ export class SysPreferenceService { }); if (!existing) return null; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } // Validate const result = ESysPreferenceSchema.safeParse(existing); if (!result.success) { - return Fail(result.error); + return Fail(FT.SysValidation, result.error); } // Return @@ -113,7 +113,7 @@ export class SysPreferenceService { ): AsyncFailable { let pref = await this.getPreference(key); if (HasFailed(pref)) return pref; - if (pref.type !== type) return Fail('Invalid preference type'); + if (pref.type !== type) return Fail(FT.UsrValidation, 'Invalid preference type'); return pref.value; } @@ -124,7 +124,7 @@ export class SysPreferenceService { SysPreferenceList.map((key) => this.getPreference(key)), ); if (internalSysPrefs.some((pref) => HasFailed(pref))) { - return Fail('Could not get all preferences'); + return Fail(FT.Internal, 'Could not get all preferences'); } return internalSysPrefs as DecodedSysPref[]; @@ -157,7 +157,7 @@ export class SysPreferenceService { // It should already be valid, but these two validators might go out of sync const result = ESysPreferenceSchema.safeParse(verifySysPreference); if (!result.success) { - return Fail(result.error); + return Fail(FT.UsrValidation, result.error); } return result.data; diff --git a/backend/src/collections/preference-db/usr-preference-db.service.ts b/backend/src/collections/preference-db/usr-preference-db.service.ts index 938e822..2c9f8dd 100644 --- a/backend/src/collections/preference-db/usr-preference-db.service.ts +++ b/backend/src/collections/preference-db/usr-preference-db.service.ts @@ -6,7 +6,7 @@ import { PrefValueTypeStrings } from 'picsur-shared/dist/dto/preferences.dto'; import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.enum'; -import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types'; import { Repository } from 'typeorm'; import { UsrPreferenceList, @@ -47,7 +47,7 @@ export class UsrPreferenceService { conflictPaths: ['key', 'user_id'], }); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } // Return @@ -80,13 +80,13 @@ export class UsrPreferenceService { }); if (!existing) return null; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } // Validate const result = EUsrPreferenceSchema.safeParse(existing); if (!result.success) { - return Fail(result.error); + return Fail(FT.SysValidation, result.error); } // Return @@ -146,7 +146,8 @@ export class UsrPreferenceService { ): AsyncFailable { let pref = await this.getPreference(userid, key); if (HasFailed(pref)) return pref; - if (pref.type !== type) return Fail('Invalid preference type'); + if (pref.type !== type) + return Fail(FT.UsrValidation, 'Invalid preference type'); return pref.value; } @@ -159,7 +160,7 @@ export class UsrPreferenceService { UsrPreferenceList.map((key) => this.getPreference(userid, key)), ); if (internalSysPrefs.some((pref) => HasFailed(pref))) { - return Fail('Could not get all preferences'); + return Fail(FT.Internal, 'Could not get all preferences'); } return internalSysPrefs as DecodedUsrPref[]; @@ -199,7 +200,7 @@ export class UsrPreferenceService { // It should already be valid, but these two validators might go out of sync const result = EUsrPreferenceSchema.safeParse(verifySysPreference); if (!result.success) { - return Fail(result.error); + return Fail(FT.UsrValidation, result.error); } return result.data; diff --git a/backend/src/collections/role-db/role-db.module.ts b/backend/src/collections/role-db/role-db.module.ts index 2b5624a..58ea97d 100644 --- a/backend/src/collections/role-db/role-db.module.ts +++ b/backend/src/collections/role-db/role-db.module.ts @@ -6,7 +6,7 @@ import { HostConfigService } from '../../config/early/host.config.service'; import { ImmutableRolesList, SystemRoleDefaults, - UndeletableRolesList + SystemRolesList } from '../../models/constants/roles.const'; import { ERoleBackend } from '../../models/entities/role.entity'; import { RolesService } from './role-db.service'; @@ -44,8 +44,7 @@ export class RolesModule implements OnModuleInit { } private async ensureSystemRolesExist() { - // The UndeletableRolesList is also the list of systemroles - for (const systemRole of UndeletableRolesList) { + for (const systemRole of SystemRolesList) { this.logger.verbose(`Ensuring system role "${systemRole}" exists`); const exists = await this.rolesService.exists(systemRole); @@ -76,7 +75,7 @@ export class RolesModule implements OnModuleInit { const result = await this.rolesService.setPermissions( immutableRole, SystemRoleDefaults[immutableRole], - true, + true, // Manual bypass for immutable roles ); if (HasFailed(result)) { this.logger.error( diff --git a/backend/src/collections/role-db/role-db.service.ts b/backend/src/collections/role-db/role-db.service.ts index 6834b23..0bcd490 100644 --- a/backend/src/collections/role-db/role-db.service.ts +++ b/backend/src/collections/role-db/role-db.service.ts @@ -4,6 +4,7 @@ import { ERoleSchema } from 'picsur-shared/dist/entities/role.entity'; import { AsyncFailable, Fail, + FT, HasFailed, HasSuccess } from 'picsur-shared/dist/types'; @@ -29,7 +30,8 @@ export class RolesService { name: string, permissions: Permissions, ): AsyncFailable { - if (await this.exists(name)) return Fail('Role already exists'); + if (await this.exists(name)) + return Fail(FT.Conflict, 'Role already exists'); let role = new ERoleBackend(); role.name = name; @@ -38,7 +40,7 @@ export class RolesService { try { return await this.rolesRepository.save(role, { reload: true }); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -47,13 +49,13 @@ export class RolesService { if (HasFailed(roleToModify)) return roleToModify; if (UndeletableRolesList.includes(roleToModify.name)) { - return Fail('Cannot delete system role'); + return Fail(FT.Permission, 'Cannot delete system role'); } try { return await this.rolesRepository.remove(roleToModify); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -109,7 +111,7 @@ export class RolesService { if (HasFailed(roleToModify)) return roleToModify; if (!allowImmutable && ImmutableRolesList.includes(roleToModify.name)) { - return Fail('Cannot modify immutable role'); + return Fail(FT.Permission, 'Cannot modify immutable role'); } roleToModify.permissions = makeUnique(permissions); @@ -117,7 +119,7 @@ export class RolesService { try { return await this.rolesRepository.save(roleToModify); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -127,10 +129,10 @@ export class RolesService { where: { name }, }); - if (!found) return Fail('Role not found'); + if (!found) return Fail(FT.NotFound, 'Role not found'); return found; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -140,20 +142,20 @@ export class RolesService { where: { name: In(names) }, }); - if (!found) return Fail('No roles found'); + if (!found) return Fail(FT.NotFound, 'No roles found'); return found; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } public async findAll(): AsyncFailable { try { const found = await this.rolesRepository.find(); - if (!found) return Fail('No roles found'); + if (!found) return Fail(FT.NotFound, 'No roles found'); return found; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -163,14 +165,17 @@ export class RolesService { public async nukeSystemRoles(IAmSure: boolean = false): AsyncFailable { if (!IAmSure) - return Fail('You must confirm that you want to delete all roles'); + return Fail( + FT.SysValidation, + 'You must confirm that you want to delete all roles', + ); try { await this.rolesRepository.delete({ name: In(UndeletableRolesList), }); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } return true; } @@ -183,7 +188,7 @@ export class RolesService { } else { const result = ERoleSchema.safeParse(role); if (!result.success) { - return Fail(result.error); + return Fail(FT.SysValidation, result.error); } // This is safe return result.data as ERoleBackend; diff --git a/backend/src/collections/user-db/user-db.service.ts b/backend/src/collections/user-db/user-db.service.ts index a4dfd9a..7233fdb 100644 --- a/backend/src/collections/user-db/user-db.service.ts +++ b/backend/src/collections/user-db/user-db.service.ts @@ -5,6 +5,7 @@ import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum'; import { AsyncFailable, Fail, + FT, HasFailed, HasSuccess } from 'picsur-shared/dist/types'; @@ -46,7 +47,8 @@ export class UsersService { // Add option to create "invalid" users, should only be used by system byPassRoleCheck?: boolean, ): AsyncFailable { - if (await this.exists(username)) return Fail('User already exists'); + if (await this.exists(username)) + return Fail(FT.Conflict, 'User already exists'); const strength = await this.getBCryptStrength(); const hashedPassword = await bcrypt.hash(password, strength); @@ -66,7 +68,7 @@ export class UsersService { try { return await this.usersRepository.save(user); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -75,13 +77,13 @@ export class UsersService { if (HasFailed(userToModify)) return userToModify; if (UndeletableUsersList.includes(userToModify.username)) { - return Fail('Cannot delete system user'); + return Fail(FT.Permission, 'Cannot delete system user'); } try { return await this.usersRepository.remove(userToModify); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -110,7 +112,7 @@ export class UsersService { try { return await this.usersRepository.save(userToModify); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -125,7 +127,7 @@ export class UsersService { .where('roles @> ARRAY[:role]', { role }) .execute(); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } return true; @@ -151,7 +153,7 @@ export class UsersService { try { userToModify = await this.usersRepository.save(userToModify); } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } return userToModify; @@ -168,11 +170,11 @@ export class UsersService { if (LockedLoginUsersList.includes(user.username)) { // Error should be kept in backend - return Fail('Wrong username'); + return Fail(FT.Authentication, 'Wrong username'); } if (!(await bcrypt.compare(password, user.hashed_password ?? ''))) - return Fail('Wrong password'); + return Fail(FT.Authentication, 'Wrong password'); return await this.findOne(user.id ?? ''); } @@ -191,10 +193,10 @@ export class UsersService { select: getPrivate ? GetCols(this.usersRepository) : undefined, }); - if (!found) return Fail('User not found'); + if (!found) return Fail(FT.NotFound, 'User not found'); return found; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -204,10 +206,10 @@ export class UsersService { where: { id: uuid }, }); - if (!found) return Fail('User not found'); + if (!found) return Fail(FT.NotFound, 'User not found'); return found as EUserBackend; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } @@ -215,8 +217,8 @@ export class UsersService { count: number, page: number, ): AsyncFailable> { - if (count < 1 || page < 0) return Fail('Invalid page'); - if (count > 100) return Fail('Too many results'); + if (count < 1 || page < 0) return Fail(FT.UsrValidation, 'Invalid page'); + if (count > 100) return Fail(FT.UsrValidation, 'Too many results'); try { const [users, amount] = await this.usersRepository.findAndCount({ @@ -224,7 +226,7 @@ export class UsersService { skip: count * page, }); - if (users === undefined) return Fail('Users not found'); + if (users === undefined) return Fail(FT.NotFound, 'Users not found'); return { results: users, @@ -233,7 +235,7 @@ export class UsersService { pages: Math.ceil(amount / count), }; } catch (e) { - return Fail(e); + return Fail(FT.Database, e); } } diff --git a/backend/src/layers/exception/exception.filter.ts b/backend/src/layers/exception/exception.filter.ts index 201d8d2..d9cf200 100644 --- a/backend/src/layers/exception/exception.filter.ts +++ b/backend/src/layers/exception/exception.filter.ts @@ -1,12 +1,7 @@ -import { - ArgumentsHost, - Catch, - ExceptionFilter, - HttpException, - Logger, -} from '@nestjs/common'; +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'; // 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) @@ -17,22 +12,35 @@ export class MainExceptionFilter implements ExceptionFilter { private static readonly logger = new Logger('MainExceptionFilter'); catch(exception: unknown, host: ArgumentsHost) { - if (exception instanceof Error) { - MainExceptionFilter.logger.warn(exception.message); - MainExceptionFilter.logger.debug(exception.stack); - } else { - MainExceptionFilter.logger.warn(exception); - } - const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); - const status = - exception instanceof HttpException ? exception.getStatus() : 500; - const message = - exception instanceof HttpException - ? exception.message - : 'Internal server error'; + + const traceString = `(${request.ip} -> ${request.method} ${request.url})`; + + if (!IsFailure(exception)) { + MainExceptionFilter.logger.error( + traceString + ' Unkown exception: ' + exception, + ); + return; + } + + if (exception.isImportant()) { + MainExceptionFilter.logger.error( + `${traceString} ${exception.getName()}: ${exception.getReason()}`, + ); + if (exception.getStack()) { + MainExceptionFilter.logger.debug(exception.getStack()); + } + } else { + MainExceptionFilter.logger.warn( + `${traceString} ${exception.getName()}: ${exception.getReason()}`, + ); + } + + const status = exception.getCode(); + const type = exception.getType(); + const message = exception.getReason(); const toSend: ApiErrorResponse = { success: false, @@ -40,6 +48,7 @@ export class MainExceptionFilter implements ExceptionFilter { timestamp: new Date().toISOString(), data: { + type, message, }, }; diff --git a/backend/src/managers/auth/auth.service.ts b/backend/src/managers/auth/auth.service.ts index 71b66bf..212941f 100644 --- a/backend/src/managers/auth/auth.service.ts +++ b/backend/src/managers/auth/auth.service.ts @@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { JwtDataSchema } from 'picsur-shared/dist/dto/jwt.dto'; import { EUser } from 'picsur-shared/dist/entities/user.entity'; -import { AsyncFailable, Fail } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types'; @Injectable() export class AuthManagerService { @@ -19,13 +19,13 @@ export class AuthManagerService { // in case of any failures const result = JwtDataSchema.safeParse(jwtData); if (!result.success) { - return Fail('Invalid JWT: ' + result.error); + return Fail(FT.SysValidation, 'Invalid JWT: ' + result.error); } try { return await this.jwtService.signAsync(result.data); } catch (e) { - return Fail("Couldn't create JWT: " + e); + return Fail(FT.Internal, "Couldn't create JWT: " + e); } } } diff --git a/backend/src/managers/auth/guards/main.guard.ts b/backend/src/managers/auth/guards/main.guard.ts index 349d21e..915bc62 100644 --- a/backend/src/managers/auth/guards/main.guard.ts +++ b/backend/src/managers/auth/guards/main.guard.ts @@ -1,14 +1,12 @@ import { - ExecutionContext, - ForbiddenException, - Injectable, + ExecutionContext, Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { EUser, EUserSchema } from 'picsur-shared/dist/entities/user.entity'; -import { Fail, Failable, HasFailed } from 'picsur-shared/dist/types'; +import { Fail, Failable, FT, HasFailed } from 'picsur-shared/dist/types'; import { UsersService } from '../../../collections/user-db/user-db.service'; import { Permissions } from '../../../models/constants/permissions.const'; import { isPermissionsArray } from '../../../models/validators/permissions.validator'; @@ -66,7 +64,7 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) { if (permissions.every((permission) => userPermissions.includes(permission))) return true; - else throw new ForbiddenException('Permission denied'); + else throw Fail(FT.Permission, 'Permission denied'); } private extractPermissions(context: ExecutionContext): Failable { @@ -79,11 +77,15 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) { if (permissions === undefined) return Fail( + FT.Internal, `${handlerName} does not have any permissions defined, denying access`, ); if (!isPermissionsArray(permissions)) - return Fail(`Permissions for ${handlerName} is not a string array`); + return Fail( + FT.Internal, + `Permissions for ${handlerName} is not a string array`, + ); return permissions; } diff --git a/backend/src/managers/image/image-converter.service.ts b/backend/src/managers/image/image-converter.service.ts index 4910ba0..66a32a8 100644 --- a/backend/src/managers/image/image-converter.service.ts +++ b/backend/src/managers/image/image-converter.service.ts @@ -6,7 +6,7 @@ import { SupportedMimeCategory } from 'picsur-shared/dist/dto/mimes.dto'; import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum'; -import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types'; import { SysPreferenceService } from '../../collections/preference-db/sys-preference-db.service'; import { SharpWrapper } from '../../workers/sharp.wrapper'; import { ImageResult } from './imageresult'; @@ -22,7 +22,10 @@ export class ImageConverterService { options: ImageRequestParams, ): AsyncFailable { if (sourcemime.type !== targetmime.type) { - return Fail("Can't convert from animated to still or vice versa"); + return Fail( + FT.Impossible, + "Can't convert from animated to still or vice versa", + ); } if (sourcemime.mime === targetmime.mime) { @@ -37,7 +40,7 @@ export class ImageConverterService { } else if (targetmime.type === SupportedMimeCategory.Animation) { return this.convertAnimation(image, targetmime, options); } else { - return Fail('Unsupported mime type'); + return Fail(FT.SysValidation, 'Unsupported mime type'); } } @@ -52,7 +55,7 @@ export class ImageConverterService { this.sysPref.getStringPreference(SysPreference.ConversionTimeLimit), ]); if (HasFailed(memLimit) || HasFailed(timeLimit)) { - return Fail('Failed to get conversion limits'); + return Fail(FT.Internal, 'Failed to get conversion limits'); } const timeLimitMS = ms(timeLimit); diff --git a/backend/src/managers/image/image-processor.service.ts b/backend/src/managers/image/image-processor.service.ts index d74df26..d358c13 100644 --- a/backend/src/managers/image/image-processor.service.ts +++ b/backend/src/managers/image/image-processor.service.ts @@ -2,9 +2,9 @@ import { Injectable } from '@nestjs/common'; import { FullMime, ImageMime, - SupportedMimeCategory, + SupportedMimeCategory } from 'picsur-shared/dist/dto/mimes.dto'; -import { AsyncFailable, Fail } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types'; import { QOIColorSpace, QOIencode } from 'qoi-img'; import { ImageResult } from './imageresult'; import { UniversalSharp } from './universal-sharp'; @@ -20,7 +20,7 @@ export class ImageProcessorService { } else if (mime.type === SupportedMimeCategory.Animation) { return await this.processAnimation(image, mime); } else { - return Fail('Unsupported mime type'); + return Fail(FT.SysValidation, 'Unsupported mime type'); } } @@ -43,7 +43,7 @@ export class ImageProcessorService { processedImage.info.width >= 32768 || processedImage.info.height >= 32768 ) { - return Fail('Image too large'); + return Fail(FT.UsrValidation, 'Image too large'); } // Png can be more efficient than QOI, but its just sooooooo slow diff --git a/backend/src/managers/image/image.service.ts b/backend/src/managers/image/image.service.ts index 5d9e805..55c3de0 100644 --- a/backend/src/managers/image/image.service.ts +++ b/backend/src/managers/image/image.service.ts @@ -6,7 +6,7 @@ import { ImageFileType } from 'picsur-shared/dist/dto/image-file-types.enum'; import { FullMime } from 'picsur-shared/dist/dto/mimes.dto'; import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum'; import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.enum'; -import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types'; import { FindResult } from 'picsur-shared/dist/types/find-result'; import { ParseMime } from 'picsur-shared/dist/util/parse-mime'; import { IsQOI } from 'qoi-img'; @@ -168,12 +168,10 @@ export class ImageManagerService { } public async getMasterMime(imageId: string): AsyncFailable { - const mime = await this.imageFilesService.getFileMimes( - imageId - ); + const mime = await this.imageFilesService.getFileMimes(imageId); if (HasFailed(mime)) return mime; - if (mime.master === undefined) return Fail('No master file'); + if (mime.master === undefined) return Fail(FT.NotFound, 'No master file'); return ParseMime(mime.master); } @@ -183,12 +181,11 @@ export class ImageManagerService { } public async getOriginalMime(imageId: string): AsyncFailable { - const mime = await this.imageFilesService.getFileMimes( - imageId - ); + const mime = await this.imageFilesService.getFileMimes(imageId); if (HasFailed(mime)) return mime; - if (mime.original === undefined) return Fail('No original file'); + if (mime.original === undefined) + return Fail(FT.NotFound, 'No original file'); return ParseMime(mime.original); } @@ -201,7 +198,7 @@ export class ImageManagerService { if (HasFailed(result)) return result; if (result[ImageFileType.MASTER] === undefined) { - return Fail('No master file found'); + return Fail(FT.NotFound, 'No master file found'); } return { diff --git a/backend/src/models/constants/roles.const.ts b/backend/src/models/constants/roles.const.ts index 89ae128..eb2d730 100644 --- a/backend/src/models/constants/roles.const.ts +++ b/backend/src/models/constants/roles.const.ts @@ -20,6 +20,9 @@ export const SoulBoundRolesList: string[] = SoulBoundRolesTuple; export const ImmutableRolesList: string[] = ImmutableRolesTuple; export const UndeletableRolesList: string[] = UndeletableRolesTuple; +// Yes this is the undeletableroles list +export const SystemRolesList = UndeletableRolesList; + // Defaults type SystemRole = typeof UndeletableRolesTuple[number]; const SystemRoleDefaultsTyped: { diff --git a/backend/src/models/dto/multipart.dto.ts b/backend/src/models/dto/multipart.dto.ts index 3f1d51f..d2180ee 100644 --- a/backend/src/models/dto/multipart.dto.ts +++ b/backend/src/models/dto/multipart.dto.ts @@ -1,5 +1,5 @@ import { MultipartFile } from '@fastify/multipart'; -import { AsyncFailable, Fail } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types'; import { z } from 'zod'; export const MultiPartFileDtoSchema = z.object({ @@ -26,7 +26,7 @@ export async function CreateMultiPartFileDto( file: file.file, }; } catch (e) { - return Fail(e); + return Fail(FT.Internal, e); } } diff --git a/backend/src/routes/api/experiment/experiment.controller.ts b/backend/src/routes/api/experiment/experiment.controller.ts index d2491c6..2aa34e7 100644 --- a/backend/src/routes/api/experiment/experiment.controller.ts +++ b/backend/src/routes/api/experiment/experiment.controller.ts @@ -7,6 +7,7 @@ import { Returns } from '../../../decorators/returns.decorator'; import type AuthFasityRequest from '../../../models/interfaces/authrequest.dto'; @Controller('api/experiment') +//@NoPermissions() @RequiredPermissions(Permission.Settings) export class ExperimentController { @Get() diff --git a/backend/src/workers/sharp.wrapper.ts b/backend/src/workers/sharp.wrapper.ts index f41baa5..7a7d144 100644 --- a/backend/src/workers/sharp.wrapper.ts +++ b/backend/src/workers/sharp.wrapper.ts @@ -7,6 +7,7 @@ import { AsyncFailable, Fail, Failable, + FT, HasFailed } from 'picsur-shared/dist/types'; import { Sharp } from 'sharp'; @@ -97,7 +98,7 @@ export class SharpWrapper { ...parameters: Parameters ): Failable { if (!this.worker) { - return Fail('Worker is not initialized'); + return Fail(FT.Internal, 'Worker is not initialized'); } const hasSent = this.sendToWorker({ @@ -120,7 +121,7 @@ export class SharpWrapper { options?: SharpWorkerFinishOptions, ): AsyncFailable { if (!this.worker) { - return Fail('Worker is not initialized'); + return Fail(FT.Internal, 'Worker is not initialized'); } const hasSent = this.sendToWorker({ @@ -158,7 +159,7 @@ export class SharpWrapper { return result.result; } catch (error) { this.purge(); - return Fail(error); + return Fail(FT.Internal, error); } } @@ -176,13 +177,13 @@ export class SharpWrapper { await pTimeout(waitReadyPromise, this.instance_timeout); return true; } catch (error) { - return Fail(error); + return Fail(FT.Internal, error); } } private sendToWorker(message: SharpWorkerSendMessage): Failable { if (!this.worker) { - return Fail('Worker is not initialized'); + return Fail(FT.Internal, 'Worker is not initialized'); } this.worker.send(message); diff --git a/frontend/src/app/models/forms/login.control.ts b/frontend/src/app/models/forms/login.control.ts index 6c770ea..2c765fc 100644 --- a/frontend/src/app/models/forms/login.control.ts +++ b/frontend/src/app/models/forms/login.control.ts @@ -1,11 +1,11 @@ import { FormControl } from '@angular/forms'; -import { Fail, Failable } from 'picsur-shared/dist/types'; +import { Fail, Failable, FT } from 'picsur-shared/dist/types'; import { UserPassModel } from '../forms-dto/userpass.dto'; import { CreatePasswordError, CreateUsernameError, PasswordValidators, - UsernameValidators, + UsernameValidators } from '../validators/user.validator'; export class LoginControl { @@ -23,7 +23,7 @@ export class LoginControl { // This getter firstly verifies the form, RawData does not public getData(): Failable { if (this.username.errors || this.password.errors) - return Fail('Invalid username or password'); + return Fail(FT.Authentication, 'Invalid username or password'); else return this.getRawData(); } diff --git a/frontend/src/app/models/forms/register.control.ts b/frontend/src/app/models/forms/register.control.ts index 2839a2a..81c6e6e 100644 --- a/frontend/src/app/models/forms/register.control.ts +++ b/frontend/src/app/models/forms/register.control.ts @@ -1,12 +1,12 @@ import { FormControl } from '@angular/forms'; -import { Fail, Failable } from 'picsur-shared/dist/types'; +import { Fail, Failable, FT } from 'picsur-shared/dist/types'; import { UserPassModel } from '../forms-dto/userpass.dto'; import { Compare } from '../validators/compare.validator'; import { CreatePasswordError, CreateUsernameError, PasswordValidators, - UsernameValidators, + UsernameValidators } from '../validators/user.validator'; export class RegisterControl { @@ -36,7 +36,7 @@ export class RegisterControl { this.password.errors || this.passwordConfirm.errors ) - return Fail('Invalid username or password'); + return Fail(FT.Authentication, 'Invalid username or password'); else return this.getRawData(); } diff --git a/frontend/src/app/services/api/api.service.ts b/frontend/src/app/services/api/api.service.ts index da8d08f..f110a25 100644 --- a/frontend/src/app/services/api/api.service.ts +++ b/frontend/src/app/services/api/api.service.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@angular/core'; import { WINDOW } from '@ng-web-apis/common'; import { ApiResponseSchema } from 'picsur-shared/dist/dto/api/api.dto'; import { Mime2Ext } from 'picsur-shared/dist/dto/mimes.dto'; -import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types'; import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto'; import { Subject } from 'rxjs'; import { ApiBuffer } from 'src/app/models/dto/api-buffer.dto'; @@ -59,7 +59,7 @@ export class ApiService { const validateResult = sendSchema.safeParse(data); if (!validateResult.success) { this.logger.error(validateResult.error); - return Fail('Something went wrong'); + return Fail(FT.SysValidation, 'Something went wrong'); } return this.fetchSafeJson(receiveType, url, { @@ -93,10 +93,11 @@ export class ApiService { const validateResult = resultSchema.safeParse(result); if (!validateResult.success) { this.logger.error(validateResult.error); - return Fail('Something went wrong'); + return Fail(FT.SysValidation, 'Something went wrong'); } - if (validateResult.data.success === false) return Fail(result.data.message); + if (validateResult.data.success === false) + return Fail(FT.Unknown, result.data.message); return validateResult.data.data; } @@ -113,7 +114,7 @@ export class ApiService { return await response.json(); } catch (e) { this.logger.error(e); - return Fail('Something went wrong'); + return Fail(FT.Internal, 'Something went wrong'); } } @@ -124,7 +125,7 @@ export class ApiService { const response = await this.fetch(url, options); if (HasFailed(response)) return response; - if (!response.ok) return Fail('Recieved a non-ok response'); + if (!response.ok) return Fail(FT.Network, 'Recieved a non-ok response'); const mimeType = response.headers.get('Content-Type') ?? 'other/unknown'; let name = response.headers.get('Content-Disposition'); @@ -150,7 +151,7 @@ export class ApiService { }; } catch (e) { this.logger.error(e); - return Fail('Something went wrong'); + return Fail(FT.Internal, 'Something went wrong'); } } @@ -161,7 +162,7 @@ export class ApiService { const response = await this.fetch(url, options); if (HasFailed(response)) return response; - if (!response.ok) return Fail('Recieved a non-ok response'); + if (!response.ok) return Fail(FT.Network, 'Recieved a non-ok response'); return response.headers; } @@ -186,7 +187,7 @@ export class ApiService { error: e, url, }); - return Fail('Network Error'); + return Fail(FT.Network, 'Network Error'); } } } diff --git a/frontend/src/app/services/api/image.service.ts b/frontend/src/app/services/api/image.service.ts index e97469f..3699166 100644 --- a/frontend/src/app/services/api/image.service.ts +++ b/frontend/src/app/services/api/image.service.ts @@ -15,7 +15,7 @@ import { ImageLinks } from 'picsur-shared/dist/dto/image-links.class'; import { Mime2Ext } from 'picsur-shared/dist/dto/mimes.dto'; import { EImage } from 'picsur-shared/dist/entities/image.entity'; import { AsyncFailable } from 'picsur-shared/dist/types'; -import { Fail, HasFailed, Open } from 'picsur-shared/dist/types/failable'; +import { Fail, FT, HasFailed, Open } from 'picsur-shared/dist/types/failable'; import { ImageUploadRequest } from '../../models/dto/image-upload-request.dto'; import { ApiService } from './api.service'; import { UserService } from './user.service'; @@ -68,7 +68,7 @@ export class ImageService { ): AsyncFailable { const userID = await this.userService.snapshot?.id; if (userID === undefined) { - return Fail('User not logged in'); + return Fail(FT.Authentication, 'User not logged in'); } return await this.ListAllImages(count, page, userID); @@ -93,6 +93,7 @@ export class ImageService { if (result.images.length !== 1) { return Fail( + FT.Unknown, `Image ${image} was not deleted, probably lacking permissions`, ); } diff --git a/frontend/src/app/services/api/info.service.ts b/frontend/src/app/services/api/info.service.ts index eb18bcc..64ef8c5 100644 --- a/frontend/src/app/services/api/info.service.ts +++ b/frontend/src/app/services/api/info.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@angular/core'; import { HISTORY } from '@ng-web-apis/common'; import { InfoResponse } from 'picsur-shared/dist/dto/api/info.dto'; -import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types'; import { SemVerRegex } from 'picsur-shared/dist/util/common-regex'; import { BehaviorSubject } from 'rxjs'; import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto'; @@ -53,7 +53,10 @@ export class InfoService { const clientVersion = this.getFrontendVersion(); if (!SemVerRegex.test(serverVersion) || !SemVerRegex.test(clientVersion)) { - return Fail(`Not a valid semver: ${serverVersion} or ${clientVersion}`); + return Fail( + FT.SysValidation, + `Not a valid semver: ${serverVersion} or ${clientVersion}`, + ); } const serverDecoded = serverVersion.split('.'); diff --git a/frontend/src/app/services/api/sys-pref.service.ts b/frontend/src/app/services/api/sys-pref.service.ts index 2e66bc5..140e055 100644 --- a/frontend/src/app/services/api/sys-pref.service.ts +++ b/frontend/src/app/services/api/sys-pref.service.ts @@ -11,7 +11,7 @@ import { DecodedPref, PrefValueType } from 'picsur-shared/dist/dto/preferences.dto'; -import { AsyncFailable, Fail, HasFailed, Map } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, FT, HasFailed, Map } from 'picsur-shared/dist/types'; import { BehaviorSubject } from 'rxjs'; import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto'; import { Throttle } from 'src/app/util/throttle'; @@ -59,7 +59,10 @@ export class SysPrefService { public async getPreferences(): AsyncFailable { if (!this.hasPermission) - return Fail('You do not have permission to edit system preferences'); + return Fail( + FT.Permission, + 'You do not have permission to edit system preferences', + ); const response = await this.api.get( MultiplePreferencesResponse, @@ -76,7 +79,10 @@ export class SysPrefService { key: string, ): AsyncFailable { if (!this.hasPermission) - return Fail('You do not have permission to edit system preferences'); + return Fail( + FT.Permission, + 'You do not have permission to edit system preferences', + ); const response = await this.api.get( GetPreferenceResponse, @@ -92,7 +98,10 @@ export class SysPrefService { value: PrefValueType, ): AsyncFailable { if (!this.hasPermission) - return Fail('You do not have permission to edit system preferences'); + return Fail( + FT.Permission, + 'You do not have permission to edit system preferences', + ); const response = await this.api.post( UpdatePreferenceRequest, diff --git a/frontend/src/app/services/api/user.service.ts b/frontend/src/app/services/api/user.service.ts index 4ce1b77..a14f30f 100644 --- a/frontend/src/app/services/api/user.service.ts +++ b/frontend/src/app/services/api/user.service.ts @@ -9,7 +9,7 @@ import { } from 'picsur-shared/dist/dto/api/user.dto'; import { JwtDataSchema } from 'picsur-shared/dist/dto/jwt.dto'; import { EUser } from 'picsur-shared/dist/entities/user.entity'; -import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types'; import { BehaviorSubject } from 'rxjs'; import { Logger } from '../logger/logger.service'; import { KeyService } from '../storage/key.service'; @@ -108,7 +108,7 @@ export class UserService { this.userSubject.next(null); if (value === null) { - return Fail('Not logged in'); + return Fail(FT.Impossible, 'Not logged in'); } else { return value; } @@ -120,13 +120,13 @@ export class UserService { try { decoded = jwt_decode(token); } catch (e) { - return Fail('Invalid token'); + return Fail(FT.UsrValidation, 'Invalid token'); } const result = JwtDataSchema.safeParse(decoded); if (!result.success) { this.logger.error(result.error); - return Fail('Invalid token data'); + return Fail(FT.UsrValidation, 'Invalid token data'); } return result.data.user; diff --git a/frontend/src/app/services/api/usr-pref.service.ts b/frontend/src/app/services/api/usr-pref.service.ts index 8c5eda8..946f48d 100644 --- a/frontend/src/app/services/api/usr-pref.service.ts +++ b/frontend/src/app/services/api/usr-pref.service.ts @@ -11,7 +11,13 @@ import { DecodedPref, PrefValueType } from 'picsur-shared/dist/dto/preferences.dto'; -import { AsyncFailable, Fail, HasFailed, Map } from 'picsur-shared/dist/types'; +import { + AsyncFailable, + Fail, + FT, + HasFailed, + Map +} from 'picsur-shared/dist/types'; import { BehaviorSubject } from 'rxjs'; import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto'; import { Throttle } from 'src/app/util/throttle'; @@ -59,7 +65,10 @@ export class UsrPrefService { public async getPreferences(): AsyncFailable { if (!this.hasPermission) - return Fail('You do not have permission to edit user preferences'); + return Fail( + FT.Permission, + 'You do not have permission to edit user preferences', + ); const response = await this.api.get( MultiplePreferencesResponse, @@ -76,7 +85,10 @@ export class UsrPrefService { key: string, ): AsyncFailable { if (!this.hasPermission) - return Fail('You do not have permission to edit user preferences'); + return Fail( + FT.Permission, + 'You do not have permission to edit user preferences', + ); const response = await this.api.get( GetPreferenceResponse, @@ -92,7 +104,10 @@ export class UsrPrefService { value: PrefValueType, ): AsyncFailable { if (!this.hasPermission) - return Fail('You do not have permission to edit user preferences'); + return Fail( + FT.Permission, + 'You do not have permission to edit user preferences', + ); const response = await this.api.post( UpdatePreferenceRequest, diff --git a/frontend/src/app/workers/qoi.job.ts b/frontend/src/app/workers/qoi.job.ts index bf53766..7b07cd8 100644 --- a/frontend/src/app/workers/qoi.job.ts +++ b/frontend/src/app/workers/qoi.job.ts @@ -1,4 +1,4 @@ -import { AsyncFailable, Fail } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types'; import { QOIdecodeJS } from '../util/qoi/qoi-decode'; import { QOIImage } from './qoi-worker.dto'; @@ -13,7 +13,7 @@ export default async function qoiDecodeJob( }, }); if (!response.ok) { - return Fail('Could not fetch image'); + return Fail(FT.Network, 'Could not fetch image'); } const buffer = await response.arrayBuffer(); @@ -32,6 +32,6 @@ export default async function qoiDecodeJob( height: image.height, }; } catch (e) { - return Fail(e); + return Fail(FT.Internal, e); } } diff --git a/shared/src/dto/api/api.dto.ts b/shared/src/dto/api/api.dto.ts index a8ee7bc..c2ecb38 100644 --- a/shared/src/dto/api/api.dto.ts +++ b/shared/src/dto/api/api.dto.ts @@ -17,6 +17,7 @@ const ApiErrorResponse = ApiResponseBase.merge( z.object({ success: z.literal(false), data: z.object({ + type: z.string(), message: z.string(), }), }), diff --git a/shared/src/types/failable.ts b/shared/src/types/failable.ts index 0791a80..cdce68f 100644 --- a/shared/src/types/failable.ts +++ b/shared/src/types/failable.ts @@ -3,20 +3,108 @@ // Since now they dont just come out of nowhere // -> Side effects go brrr +// Failuretype +export enum FT { + Unknown = 'unknown', + Database = 'database', + SysValidation = 'sysvalidation', + UsrValidation = 'usrvalidation', + Permission = 'permission', + NotFound = 'notFound', + Conflict = 'conflict', + Internal = 'internal', + Authentication = 'authentication', + Impossible = 'impossible', + Network = 'network', +} + +interface FTProp { + important: boolean; + code: number; +} + +const FTProps: { + [key in FT]: FTProp; +} = { + [FT.Unknown]: { + important: false, + code: 500, + }, + [FT.Internal]: { + important: true, + code: 500, + }, + [FT.Database]: { + important: true, + code: 500, + }, + [FT.Network]: { + important: true, + code: 500, + }, + [FT.SysValidation]: { + important: true, + code: 500, + }, + [FT.UsrValidation]: { + important: false, + code: 400, + }, + [FT.Permission]: { + important: false, + code: 403, + }, + [FT.NotFound]: { + important: false, + code: 404, + }, + [FT.Conflict]: { + important: false, + code: 409, + }, + [FT.Authentication]: { + important: false, + code: 200, + } , + [FT.Impossible]: { + important: true, + code: 422, + } , +}; + export class Failure { private __68351953531423479708__id_failure = 1148363914; constructor( private readonly reason?: string, private readonly stack?: string, + private readonly type: FT = FT.Unknown, ) {} getReason(): string { return this.reason ?? 'Unknown'; } - getStack(): string { - return this.stack ?? 'None'; + getStack(): string | undefined { + return this.stack; + } + + getType(): FT { + return this.type; + } + + getName(): string { + const capitalizedType = + this.type.charAt(0).toUpperCase() + this.type.slice(1); + return `${capitalizedType}Failure`; + } + + getCode(): number { + return FTProps[this.type].code; + } + + isImportant() { + return FTProps[this.type].important; } static deserialize(data: any): Failure { @@ -24,22 +112,26 @@ export class Failure { throw new Error('Invalid failure data'); } - return new Failure(data.reason, data.stack); + return new Failure(data.reason, data.stack, data.type); } } -export function Fail(reason?: any): Failure { +export function Fail(type: FT, reason: any): Failure { if (typeof reason === 'string') { - return new Failure(reason); + return new Failure(reason, undefined, type); } else if (reason instanceof Error) { - return new Failure(reason.message, reason.stack); + return new Failure(reason.message, reason.stack, type); } else if (reason instanceof Failure) { - return reason; + throw new Error('Cannot fail with a failure, just return it'); } else { - return new Failure('Converted(' + reason + ')'); + return new Failure('Unkown reason', undefined, type); } } +export function IsFailure(value: any): value is Failure { + return value.__68351953531423479708__id_failure === 1148363914; +} + export type Failable = T | Failure; export type AsyncFailable = Promise>; diff --git a/shared/src/util/parse-mime.ts b/shared/src/util/parse-mime.ts index 5794ea0..3d20068 100644 --- a/shared/src/util/parse-mime.ts +++ b/shared/src/util/parse-mime.ts @@ -4,7 +4,7 @@ import { SupportedImageMimes, SupportedMimeCategory } from '../dto/mimes.dto'; -import { Fail, Failable } from '../types'; +import { Fail, Failable, FT } from '../types'; export function ParseMime(mime: string): Failable { if (SupportedImageMimes.includes(mime)) @@ -13,5 +13,5 @@ export function ParseMime(mime: string): Failable { if (SupportedAnimMimes.includes(mime)) return { mime, type: SupportedMimeCategory.Animation }; - return Fail('Unsupported mime type'); + return Fail(FT.Validation, 'Unsupported mime type'); }