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 { JwtService } from '@nestjs/jwt';
import { instanceToPlain, plainToClass } from 'class-transformer'; import { instanceToPlain, plainToClass } from 'class-transformer';
import { JwtDataDto } from 'picsur-shared/dist/dto/jwt.dto'; 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 { strictValidate } from 'picsur-shared/dist/util/validate';
import { EUserBackend } from '../../models/entities/user.entity'; import { EUserBackend } from '../../models/entities/user.entity';
@@ -11,7 +12,7 @@ export class AuthManagerService {
constructor(private jwtService: JwtService) {} constructor(private jwtService: JwtService) {}
async createToken(user: EUserBackend): Promise<string> { async createToken(user: EUserBackend): AsyncFailable<string> {
const jwtData: JwtDataDto = plainToClass(JwtDataDto, { const jwtData: JwtDataDto = plainToClass(JwtDataDto, {
user: { user: {
username: user.username, username: user.username,
@@ -23,10 +24,13 @@ export class AuthManagerService {
// in case of any failures // in case of any failures
const errors = await strictValidate(jwtData); const errors = await strictValidate(jwtData);
if (errors.length > 0) { if (errors.length > 0) {
this.logger.warn(errors); return Fail('Invalid JWT: ' + errors);
throw new Error('Invalid jwt token generated');
} }
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 { UserRolesService } from '../../../collections/userdb/userrolesdb.service';
import { Permissions } from '../../../models/dto/permissions.dto'; import { Permissions } from '../../../models/dto/permissions.dto';
import { EUserBackend } from '../../../models/entities/user.entity'; 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 // 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 // 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 { MultiPartFileDto } from './multipart.dto';
import { IsMultiPartFile } from './multipart.validator';
// A validation class for form based file upload of an image // A validation class for form based file upload of an image
export class ImageUploadDto { export class ImageUploadDto {

View File

@@ -1,7 +1,8 @@
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsDefined, ValidateNested } from 'class-validator'; import { IsDefined, ValidateNested } from 'class-validator';
import { CombinePDecorators } from 'picsur-shared/dist/util/decorator'; 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( export const IsMultiPartFile = CombinePDecorators(
IsDefined(), IsDefined(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,9 +39,13 @@ export class UserController {
@Post('login') @Post('login')
@UseLocalAuth(Permission.UserLogin) @UseLocalAuth(Permission.UserLogin)
async login(@Request() req: AuthFasityRequest): Promise<UserLoginResponse> { async login(@Request() req: AuthFasityRequest): Promise<UserLoginResponse> {
return { const jwt_token = await this.authService.createToken(req.user);
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') @Post('register')
@@ -71,10 +75,13 @@ export class UserController {
throw new InternalServerErrorException('Could not get user'); throw new InternalServerErrorException('Could not get user');
} }
return { const token = await this.authService.createToken(user);
user, if (HasFailed(token)) {
token: await this.authService.createToken(user), this.logger.warn(token.getReason());
}; throw new InternalServerErrorException('Could not get new token');
}
return { user, token };
} }
// You can always check your permissions // 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 { import {
BadRequestException,
Controller, Controller,
Get, Get,
InternalServerErrorException, InternalServerErrorException,
Logger, Logger,
NotFoundException, NotFoundException,
Param, Param,
Post, Post, Res
Req,
Res
} from '@nestjs/common'; } from '@nestjs/common';
import { isHash } from 'class-validator'; import { FastifyReply } from 'fastify';
import { FastifyReply, FastifyRequest } from 'fastify';
import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto'; import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { MultiPart } from '../../decorators/multipart.decorator'; import { MultiPart } from '../../decorators/multipart.decorator';
@@ -19,6 +15,7 @@ import { RequiredPermissions } from '../../decorators/permissions.decorator';
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service'; import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
import { Permission } from '../../models/dto/permissions.dto'; import { Permission } from '../../models/dto/permissions.dto';
import { ImageUploadDto } from '../../models/requests/imageroute.dto'; import { ImageUploadDto } from '../../models/requests/imageroute.dto';
import { ImageIdValidator } from './imageid.validator';
@Controller('i') @Controller('i')
@RequiredPermissions(Permission.ImageView) @RequiredPermissions(Permission.ImageView)
@@ -29,11 +26,11 @@ export class ImageController {
@Get(':hash') @Get(':hash')
async getImage( 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, @Res({ passthrough: true }) res: FastifyReply,
@Param('hash') hash: string, @Param('hash', ImageIdValidator) hash: string,
): Promise<Buffer> { ): Promise<Buffer> {
if (!isHash(hash, 'sha256')) throw new BadRequestException('Invalid hash');
const image = await this.imagesService.retrieveComplete(hash); const image = await this.imagesService.retrieveComplete(hash);
if (HasFailed(image)) { if (HasFailed(image)) {
this.logger.warn(image.getReason()); this.logger.warn(image.getReason());
@@ -45,9 +42,9 @@ export class ImageController {
} }
@Get('meta/:hash') @Get('meta/:hash')
async getImageMeta(@Param('hash') hash: string): Promise<ImageMetaResponse> { async getImageMeta(
if (!isHash(hash, 'sha256')) throw new BadRequestException('Invalid hash'); @Param('hash', ImageIdValidator) hash: string,
): Promise<ImageMetaResponse> {
const image = await this.imagesService.retrieveInfo(hash); const image = await this.imagesService.retrieveInfo(hash);
if (HasFailed(image)) { if (HasFailed(image)) {
this.logger.warn(image.getReason()); this.logger.warn(image.getReason());
@@ -60,7 +57,6 @@ export class ImageController {
@Post() @Post()
@RequiredPermissions(Permission.ImageUpload) @RequiredPermissions(Permission.ImageUpload)
async uploadImage( async uploadImage(
@Req() req: FastifyRequest,
@MultiPart(ImageUploadDto) multipart: ImageUploadDto, @MultiPart(ImageUploadDto) multipart: ImageUploadDto,
): Promise<ImageMetaResponse> { ): Promise<ImageMetaResponse> {
const fileBuffer = await multipart.image.toBuffer(); const fileBuffer = await multipart.image.toBuffer();

View File

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