Change failure behaviour

This commit is contained in:
rubikscraft 2022-07-04 17:11:42 +02:00
parent ba47d0bff4
commit c8722d8944
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
30 changed files with 320 additions and 174 deletions

View file

@ -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<FindResult<EImageBackend>> {
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<EImageBackend[]> {
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<true> {
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;
}

View file

@ -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);
}
}
}

View file

@ -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<E extends Enum>(
@ -88,7 +89,7 @@ export class PreferenceCommonService {
): Failable<V> {
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<string> {
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');
}
}

View file

@ -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<PrefValueType> {
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;

View file

@ -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<PrefValueType> {
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;

View file

@ -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(

View file

@ -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<ERoleBackend> {
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<ERoleBackend[]> {
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<true> {
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;

View file

@ -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<EUserBackend> {
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<FindResult<EUserBackend>> {
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);
}
}

View file

@ -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<FastifyReply>();
const request = ctx.getRequest<FastifyRequest>();
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,
},
};

View file

@ -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);
}
}
}

View file

@ -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<Permissions> {
@ -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;
}

View file

@ -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<ImageResult> {
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);

View file

@ -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

View file

@ -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<FullMime> {
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<FullMime> {
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 {

View file

@ -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: {

View file

@ -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);
}
}

View file

@ -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()

View file

@ -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<Sharp[Operation]>
): Failable<true> {
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<SharpResult> {
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<true> {
if (!this.worker) {
return Fail('Worker is not initialized');
return Fail(FT.Internal, 'Worker is not initialized');
}
this.worker.send(message);

View file

@ -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<UserPassModel> {
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();
}

View file

@ -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();
}

View file

@ -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');
}
}
}

View file

@ -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<ImageListResponse> {
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`,
);
}

View file

@ -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('.');

View file

@ -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<DecodedPref[]> {
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<GetPreferenceResponse> {
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<UpdatePreferenceResponse> {
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,

View file

@ -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;

View file

@ -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<DecodedPref[]> {
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<GetPreferenceResponse> {
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<UpdatePreferenceResponse> {
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,

View file

@ -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);
}
}

View file

@ -17,6 +17,7 @@ const ApiErrorResponse = ApiResponseBase.merge(
z.object({
success: z.literal(false),
data: z.object({
type: z.string(),
message: z.string(),
}),
}),

View file

@ -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> = T | Failure;
export type AsyncFailable<T> = Promise<Failable<T>>;

View file

@ -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<FullMime> {
if (SupportedImageMimes.includes(mime))
@ -13,5 +13,5 @@ export function ParseMime(mime: string): Failable<FullMime> {
if (SupportedAnimMimes.includes(mime))
return { mime, type: SupportedMimeCategory.Animation };
return Fail('Unsupported mime type');
return Fail(FT.Validation, 'Unsupported mime type');
}