mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-12 14:55:39 +01:00
refactor backend done
This commit is contained in:
18
backend/src/decorators/returns.decorator.ts
Normal file
18
backend/src/decorators/returns.decorator.ts
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
17
backend/src/routes/image/imageid.validator.ts
Normal file
17
backend/src/routes/image/imageid.validator.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user