refactor backend done

This commit is contained in:
rubikscraft
2022-03-28 15:43:52 +02:00
parent ee5db6cd12
commit 31eac94bc7
15 changed files with 95 additions and 61 deletions

View File

@@ -0,0 +1,18 @@
import { SetMetadata } from '@nestjs/common';
import { Newable } from 'picsur-shared/dist/types/newable';
// Not yet used, but can be used for outgoing data validation
type ReturnsMethodDecorator<ReturnType> = <
T extends (...args: any) => ReturnType | Promise<ReturnType>,
>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>,
) => TypedPropertyDescriptor<T> | void;
export function Returns<N extends Object>(
newable: Newable<N>,
): ReturnsMethodDecorator<N> {
return SetMetadata('returns', newable);
}

View File

@@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { instanceToPlain, plainToClass } from 'class-transformer';
import { JwtDataDto } from 'picsur-shared/dist/dto/jwt.dto';
import { AsyncFailable, Fail } from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { EUserBackend } from '../../models/entities/user.entity';
@@ -11,7 +12,7 @@ export class AuthManagerService {
constructor(private jwtService: JwtService) {}
async createToken(user: EUserBackend): Promise<string> {
async createToken(user: EUserBackend): AsyncFailable<string> {
const jwtData: JwtDataDto = plainToClass(JwtDataDto, {
user: {
username: user.username,
@@ -23,10 +24,13 @@ export class AuthManagerService {
// in case of any failures
const errors = await strictValidate(jwtData);
if (errors.length > 0) {
this.logger.warn(errors);
throw new Error('Invalid jwt token generated');
return Fail('Invalid JWT: ' + errors);
}
return this.jwtService.signAsync(instanceToPlain(jwtData));
try {
return await this.jwtService.signAsync(instanceToPlain(jwtData));
} catch (e) {
return Fail("Couldn't create JWT: " + e);
}
}
}

View File

@@ -13,7 +13,7 @@ import { strictValidate } from 'picsur-shared/dist/util/validate';
import { UserRolesService } from '../../../collections/userdb/userrolesdb.service';
import { Permissions } from '../../../models/dto/permissions.dto';
import { EUserBackend } from '../../../models/entities/user.entity';
import { isPermissionsArray } from '../../../models/util/permissions.validator';
import { isPermissionsArray } from '../../../models/validators/permissions.validator';
// This guard extends both the jwt authenticator and the guest authenticator
// The order matters here, because this results in the guest authenticator being used as a fallback

View File

@@ -1,5 +1,5 @@
import { IsMultiPartFile } from '../validators/multipart.validator';
import { MultiPartFileDto } from './multipart.dto';
import { IsMultiPartFile } from './multipart.validator';
// A validation class for form based file upload of an image
export class ImageUploadDto {

View File

@@ -1,7 +1,8 @@
import { Type } from 'class-transformer';
import { IsDefined, ValidateNested } from 'class-validator';
import { CombinePDecorators } from 'picsur-shared/dist/util/decorator';
import { MultiPartFieldDto, MultiPartFileDto } from './multipart.dto';
import { MultiPartFieldDto, MultiPartFileDto } from '../requests/multipart.dto';
export const IsMultiPartFile = CombinePDecorators(
IsDefined(),

View File

@@ -1,11 +1,9 @@
import { Controller, Get, Request } from '@nestjs/common';
import AuthFasityRequest from '../../../models/requests/authrequest.dto';
@Controller('api/experiment')
export class ExperimentController {
@Get()
// @Guest()
async testRoute(@Request() req: AuthFasityRequest) {
return {
message: req.user,

View File

@@ -1,6 +1,9 @@
import { Module } from '@nestjs/common';
import { ExperimentController } from './experiment.controller';
// This is comletely useless module, but is used for testing
// TODO: remove when out of beta
@Module({
controllers: [ExperimentController]
})

View File

@@ -1,6 +1,9 @@
import { Controller, Get } from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import { AllPermissionsResponse, InfoResponse } from 'picsur-shared/dist/dto/api/info.dto';
import {
AllPermissionsResponse,
InfoResponse
} from 'picsur-shared/dist/dto/api/info.dto';
import { HostConfigService } from '../../../config/early/host.config.service';
import { NoPermissions } from '../../../decorators/permissions.decorator';
import { PermissionsList } from '../../../models/dto/permissions.dto';
@@ -20,7 +23,7 @@ export class InfoController {
}
// List all available permissions
@Get('/permissions')
@Get('permissions')
async getPermissions(): Promise<AllPermissionsResponse> {
const result: AllPermissionsResponse = {
Permissions: PermissionsList,

View File

@@ -7,12 +7,9 @@ import {
Param,
Post
} from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import {
GetSyspreferenceResponse,
MultipleSysPreferencesResponse,
SysPreferenceBaseResponse,
UpdateSysPreferenceRequest,
MultipleSysPreferencesResponse, UpdateSysPreferenceRequest,
UpdateSysPreferenceResponse
} from 'picsur-shared/dist/dto/api/pref.dto';
import { HasFailed } from 'picsur-shared/dist/types';
@@ -35,14 +32,10 @@ export class PrefController {
throw new InternalServerErrorException('Could not get preferences');
}
const returned: MultipleSysPreferencesResponse = {
preferences: prefs.map((pref) =>
plainToClass(SysPreferenceBaseResponse, pref),
),
return {
preferences: prefs,
total: prefs.length,
};
return plainToClass(MultipleSysPreferencesResponse, returned);
}
@Get('sys/:key')
@@ -55,7 +48,7 @@ export class PrefController {
throw new InternalServerErrorException('Could not get preference');
}
return plainToClass(GetSyspreferenceResponse, pref);
return pref;
}
@Post('sys/:key')
@@ -71,12 +64,10 @@ export class PrefController {
throw new InternalServerErrorException('Could not set preference');
}
const returned = {
return {
key,
value: pref.value,
type: pref.type,
};
return plainToClass(UpdateSysPreferenceResponse, returned);
}
}

View File

@@ -6,7 +6,6 @@ import {
Logger,
Post
} from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import {
RoleCreateRequest,
RoleCreateResponse,
@@ -22,16 +21,14 @@ import {
import { HasFailed } from 'picsur-shared/dist/types';
import { RolesService } from '../../../collections/roledb/roledb.service';
import { RequiredPermissions } from '../../../decorators/permissions.decorator';
import {
Permission
} from '../../../models/dto/permissions.dto';
import { Permission } from '../../../models/dto/permissions.dto';
import {
DefaultRolesList,
ImmutableRolesList,
SoulBoundRolesList,
UndeletableRolesList
} from '../../../models/dto/roles.dto';
import { isPermissionsArray } from '../../../models/util/permissions.validator';
import { isPermissionsArray } from '../../../models/validators/permissions.validator';
@Controller('api/roles')
@RequiredPermissions(Permission.RoleManage)
@@ -40,7 +37,7 @@ export class RolesController {
constructor(private rolesService: RolesService) {}
@Get('/list')
@Get('list')
async getRoles(): Promise<RoleListResponse> {
const roles = await this.rolesService.findAll();
if (HasFailed(roles)) {
@@ -54,7 +51,7 @@ export class RolesController {
};
}
@Post('/info')
@Post('info')
async getRole(@Body() body: RoleInfoRequest): Promise<RoleInfoResponse> {
const role = await this.rolesService.findOne(body.name);
if (HasFailed(role)) {
@@ -65,13 +62,13 @@ export class RolesController {
return role;
}
@Post('/update')
@Post('update')
async updateRole(
@Body() body: RoleUpdateRequest,
): Promise<RoleUpdateResponse> {
const permissions = body.permissions;
if (!isPermissionsArray(permissions)) {
throw new InternalServerErrorException('Invalid permissions array');
throw new InternalServerErrorException('Invalid permissions');
}
const updatedRole = await this.rolesService.setPermissions(
@@ -86,7 +83,7 @@ export class RolesController {
return updatedRole;
}
@Post('/create')
@Post('create')
async createRole(
@Body() role: RoleCreateRequest,
): Promise<RoleCreateResponse> {
@@ -104,7 +101,7 @@ export class RolesController {
return newRole;
}
@Post('/delete')
@Post('delete')
async deleteRole(
@Body() role: RoleDeleteRequest,
): Promise<RoleDeleteResponse> {
@@ -117,16 +114,13 @@ export class RolesController {
return deletedRole;
}
@Get('/special')
@Get('special')
async getSpecialRoles(): Promise<SpecialRolesResponse> {
const result: SpecialRolesResponse = {
return {
SoulBoundRoles: SoulBoundRolesList,
ImmutableRoles: ImmutableRolesList,
UndeletableRoles: UndeletableRolesList,
DefaultRoles: DefaultRolesList,
};
return plainToClass(SpecialRolesResponse, result);
}
}

View File

@@ -39,9 +39,13 @@ export class UserController {
@Post('login')
@UseLocalAuth(Permission.UserLogin)
async login(@Request() req: AuthFasityRequest): Promise<UserLoginResponse> {
return {
jwt_token: await this.authService.createToken(req.user),
};
const jwt_token = await this.authService.createToken(req.user);
if (HasFailed(jwt_token)) {
this.logger.warn(jwt_token.getReason());
throw new InternalServerErrorException('Could not get new token');
}
return { jwt_token };
}
@Post('register')
@@ -71,10 +75,13 @@ export class UserController {
throw new InternalServerErrorException('Could not get user');
}
return {
user,
token: await this.authService.createToken(user),
};
const token = await this.authService.createToken(user);
if (HasFailed(token)) {
this.logger.warn(token.getReason());
throw new InternalServerErrorException('Could not get new token');
}
return { user, token };
}
// You can always check your permissions

View File

@@ -0,0 +1,17 @@
import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform
} from '@nestjs/common';
import { isHash } from 'class-validator';
@Injectable()
export class ImageIdValidator implements PipeTransform<string, string> {
transform(value: string, metadata: ArgumentMetadata): string {
if (isHash(value, 'sha256')) {
return value;
}
throw new BadRequestException('Invalid image id');
}
}

View File

@@ -1,17 +1,13 @@
import {
BadRequestException,
Controller,
Get,
InternalServerErrorException,
Logger,
NotFoundException,
Param,
Post,
Req,
Res
Post, Res
} from '@nestjs/common';
import { isHash } from 'class-validator';
import { FastifyReply, FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto';
import { HasFailed } from 'picsur-shared/dist/types';
import { MultiPart } from '../../decorators/multipart.decorator';
@@ -19,6 +15,7 @@ import { RequiredPermissions } from '../../decorators/permissions.decorator';
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
import { Permission } from '../../models/dto/permissions.dto';
import { ImageUploadDto } from '../../models/requests/imageroute.dto';
import { ImageIdValidator } from './imageid.validator';
@Controller('i')
@RequiredPermissions(Permission.ImageView)
@@ -29,11 +26,11 @@ export class ImageController {
@Get(':hash')
async getImage(
// Usually passthrough is for manually sending the response,
// But we need it here to set the mime type
@Res({ passthrough: true }) res: FastifyReply,
@Param('hash') hash: string,
@Param('hash', ImageIdValidator) hash: string,
): Promise<Buffer> {
if (!isHash(hash, 'sha256')) throw new BadRequestException('Invalid hash');
const image = await this.imagesService.retrieveComplete(hash);
if (HasFailed(image)) {
this.logger.warn(image.getReason());
@@ -45,9 +42,9 @@ export class ImageController {
}
@Get('meta/:hash')
async getImageMeta(@Param('hash') hash: string): Promise<ImageMetaResponse> {
if (!isHash(hash, 'sha256')) throw new BadRequestException('Invalid hash');
async getImageMeta(
@Param('hash', ImageIdValidator) hash: string,
): Promise<ImageMetaResponse> {
const image = await this.imagesService.retrieveInfo(hash);
if (HasFailed(image)) {
this.logger.warn(image.getReason());
@@ -60,7 +57,6 @@ export class ImageController {
@Post()
@RequiredPermissions(Permission.ImageUpload)
async uploadImage(
@Req() req: FastifyRequest,
@MultiPart(ImageUploadDto) multipart: ImageUploadDto,
): Promise<ImageMetaResponse> {
const fileBuffer = await multipart.image.toBuffer();

View File

@@ -1,10 +1,12 @@
import { Module } from '@nestjs/common';
import { DecoratorsModule } from '../../decorators/decorators.module';
import { ImageManagerModule } from '../../managers/imagemanager/imagemanager.module';
import { ImageIdValidator } from './imageid.validator';
import { ImageController } from './imageroute.controller';
@Module({
imports: [ImageManagerModule, DecoratorsModule],
providers: [ImageIdValidator],
controllers: [ImageController],
})
export class ImageModule {}