From 86cbbdd5b43183946e6bbe0feaf7d7e6335a9507 Mon Sep 17 00:00:00 2001 From: rubikscraft Date: Mon, 25 Apr 2022 18:07:36 +0200 Subject: [PATCH] Fix some cors issues --- backend/src/app.module.ts | 35 +++++++++++++++++-- .../image-db/image-file-db.service.ts | 26 ++++++++++++-- backend/src/main.ts | 1 - backend/src/managers/image/image.service.ts | 24 +++++++++++-- .../src/models/entities/image-file.entity.ts | 2 +- backend/src/routes/image/image.controller.ts | 17 ++++++--- backend/src/routes/image/image.module.ts | 14 ++------ .../settings/settings.routing.module.ts | 1 + .../src/app/routes/view/view.component.html | 2 +- .../src/app/routes/view/view.component.ts | 12 ++++++- .../src/app/services/api/image.service.ts | 20 ++++++----- shared/src/dto/api/image.dto.ts | 15 +++++++- .../src/dto/image-file-types.dto.ts | 0 13 files changed, 134 insertions(+), 35 deletions(-) rename backend/src/models/constants/image-file-types.const.ts => shared/src/dto/image-file-types.dto.ts (100%) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index b70a24d..af5337f 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,6 +1,8 @@ -import { Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { ServeStaticModule } from '@nestjs/serve-static'; import { TypeOrmModule } from '@nestjs/typeorm'; +import cors from 'cors'; +import { IncomingMessage, ServerResponse } from 'http'; import { EarlyConfigModule } from './config/early/early-config.module'; import { ServeStaticConfigService } from './config/early/serve-static.config.service'; import { TypeOrmConfigService } from './config/early/type-orm.config.service'; @@ -9,6 +11,30 @@ import { AuthManagerModule } from './managers/auth/auth.module'; import { DemoManagerModule } from './managers/demo/demo.module'; import { PicsurRoutesModule } from './routes/routes.module'; +const mainCorsConfig = cors({ + origin: '', +}); + +const imageCorsConfig = cors({ + origin: '*', + methods: ['GET', 'HEAD', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'Accept'], + exposedHeaders: ['Content-Type', 'Authorization', 'Accept'], + credentials: false, + // A month + maxAge: 30 * 24 * 60 * 60, +}); + +const imageCorpOverride = ( + req: IncomingMessage, + res: ServerResponse, + next: Function, +) => { + res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); + + next(); +}; + @Module({ imports: [ TypeOrmModule.forRootAsync({ @@ -25,4 +51,9 @@ import { PicsurRoutesModule } from './routes/routes.module'; PicsurRoutesModule, ], }) -export class AppModule {} +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(mainCorsConfig).exclude('/i').forRoutes('/'); + consumer.apply(imageCorsConfig, imageCorpOverride).forRoutes('/i'); + } +} 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 8ec6dcc..81bfb80 100644 --- a/backend/src/collections/image-db/image-file-db.service.ts +++ b/backend/src/collections/image-db/image-file-db.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { ImageFileType } from 'picsur-shared/dist/dto/image-file-types.dto'; import { AsyncFailable, Fail } from 'picsur-shared/dist/types'; -import { LessThan, Repository } from 'typeorm'; -import { ImageFileType } from '../../models/constants/image-file-types.const'; +import { In, LessThan, Repository } from 'typeorm'; import { EImageDerivativeBackend } from '../../models/entities/image-derivative.entity'; import { EImageFileBackend } from '../../models/entities/image-file.entity'; @@ -74,6 +74,28 @@ export class ImageFileDBService { } } + public async getFileMimes( + imageId: string, + types: ImageFileType[], + ): AsyncFailable<{ [key: string]: string | undefined }> { + try { + const found = await this.imageFileRepo.find({ + where: { image_id: imageId, type: In(types) }, + select: ['type', 'mime'], + }); + + if (!found) return Fail('Image not found'); + return Object.fromEntries( + types.map((type) => [ + type, + found.find((f) => f.type === type)?.mime, + ]), + ); + } catch (e) { + return Fail(e); + } + } + public async addDerivative( imageId: string, key: string, diff --git a/backend/src/main.ts b/backend/src/main.ts index f99faca..74f96ed 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -31,7 +31,6 @@ async function bootstrap() { }, ); - // Configure nest app app.useGlobalFilters(new MainExceptionFilter()); app.useGlobalInterceptors(new SuccessInterceptor(app.get(Reflector))); app.useGlobalPipes(new ZodValidationPipe()); diff --git a/backend/src/managers/image/image.service.ts b/backend/src/managers/image/image.service.ts index 049ddbb..e135f22 100644 --- a/backend/src/managers/image/image.service.ts +++ b/backend/src/managers/image/image.service.ts @@ -1,15 +1,15 @@ import { Injectable, Logger } from '@nestjs/common'; import Crypto from 'crypto'; import { fileTypeFromBuffer, FileTypeResult } from 'file-type'; +import { ImageFileType } from 'picsur-shared/dist/dto/image-file-types.dto'; import { FullMime } from 'picsur-shared/dist/dto/mimes.dto'; import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.dto'; -import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types'; +import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types'; import { ParseMime } from 'picsur-shared/dist/util/parse-mime'; import { IsQOI } from 'qoi-img'; import { ImageDBService } from '../../collections/image-db/image-db.service'; import { ImageFileDBService } from '../../collections/image-db/image-file-db.service'; import { UsrPreferenceService } from '../../collections/preference-db/usr-preference-db.service'; -import { ImageFileType } from '../../models/constants/image-file-types.const'; import { EImageDerivativeBackend } from '../../models/entities/image-derivative.entity'; import { EImageFileBackend } from '../../models/entities/image-file.entity'; import { EImageBackend } from '../../models/entities/image.entity'; @@ -157,6 +157,26 @@ export class ImageManagerService { return ParseMime(mime); } + public async getAllFileMimes(imageId: string): AsyncFailable<{ + [ImageFileType.MASTER]: string; + [ImageFileType.ORIGINAL]: string | undefined; + }> { + const result = await this.imageFilesService.getFileMimes(imageId, [ + ImageFileType.MASTER, + ImageFileType.ORIGINAL, + ]); + if (HasFailed(result)) return result; + + if (result[ImageFileType.MASTER] === undefined) { + return Fail('No master file found'); + } + + return { + [ImageFileType.MASTER]: result[ImageFileType.MASTER]!, + [ImageFileType.ORIGINAL]: result[ImageFileType.ORIGINAL], + }; + } + // Util stuff ================================================================== private async getFullMimeFromBuffer(image: Buffer): AsyncFailable { diff --git a/backend/src/models/entities/image-file.entity.ts b/backend/src/models/entities/image-file.entity.ts index 2287042..8d4cea6 100644 --- a/backend/src/models/entities/image-file.entity.ts +++ b/backend/src/models/entities/image-file.entity.ts @@ -1,5 +1,5 @@ +import { ImageFileType } from 'picsur-shared/dist/dto/image-file-types.dto'; import { Column, Entity, Index, PrimaryGeneratedColumn, Unique } from 'typeorm'; -import { ImageFileType } from '../constants/image-file-types.const'; @Entity() @Unique(['image_id', 'type']) diff --git a/backend/src/routes/image/image.controller.ts b/backend/src/routes/image/image.controller.ts index 001a8a7..27b9316 100644 --- a/backend/src/routes/image/image.controller.ts +++ b/backend/src/routes/image/image.controller.ts @@ -9,7 +9,10 @@ import { Res } from '@nestjs/common'; import { FastifyReply } from 'fastify'; -import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto'; +import { + ImageMetaResponse, + ImageUploadResponse +} from 'picsur-shared/dist/dto/api/image.dto'; import { HasFailed } from 'picsur-shared/dist/types'; import { ImageFullIdParam } from '../../decorators/image-id/image-full-id.decorator'; import { ImageIdParam } from '../../decorators/image-id/image-id.decorator'; @@ -88,16 +91,22 @@ export class ImageController { throw new NotFoundException('Could not find image'); } - return image; + const fileMimes = await this.imagesService.getAllFileMimes(id); + if (HasFailed(fileMimes)) { + this.logger.warn(fileMimes.getReason()); + throw new InternalServerErrorException('Could not get image mime'); + } + + return { image, fileMimes }; } @Post() - @Returns(ImageMetaResponse) + @Returns(ImageUploadResponse) @RequiredPermissions(Permission.ImageUpload) async uploadImage( @MultiPart() multipart: ImageUploadDto, @ReqUserID() userid: string, - ): Promise { + ): Promise { const image = await this.imagesService.upload( multipart.image.buffer, userid, diff --git a/backend/src/routes/image/image.module.ts b/backend/src/routes/image/image.module.ts index c4ad70e..6626c9f 100644 --- a/backend/src/routes/image/image.module.ts +++ b/backend/src/routes/image/image.module.ts @@ -1,20 +1,10 @@ -import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; -import cors from 'cors'; +import { Module } from '@nestjs/common'; import { DecoratorsModule } from '../../decorators/decorators.module'; import { ImageManagerModule } from '../../managers/image/image.module'; import { ImageController } from './image.controller'; -const corsConfig = cors({ - // 48 hours - maxAge: 1728000, -}); - @Module({ imports: [ImageManagerModule, DecoratorsModule], controllers: [ImageController], }) -export class ImageModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - consumer.apply(corsConfig).forRoutes('/i'); - } -} +export class ImageModule {} diff --git a/frontend/src/app/routes/settings/settings.routing.module.ts b/frontend/src/app/routes/settings/settings.routing.module.ts index 2b9d8b1..30eb37a 100644 --- a/frontend/src/app/routes/settings/settings.routing.module.ts +++ b/frontend/src/app/routes/settings/settings.routing.module.ts @@ -16,6 +16,7 @@ const SettingsRoutes: PRoutes = [ children: [ { path: '', + pathMatch: 'full', redirectTo: 'general', }, { diff --git a/frontend/src/app/routes/view/view.component.html b/frontend/src/app/routes/view/view.component.html index 5a1b09c..f172bfd 100644 --- a/frontend/src/app/routes/view/view.component.html +++ b/frontend/src/app/routes/view/view.component.html @@ -5,7 +5,7 @@
- +
diff --git a/frontend/src/app/routes/view/view.component.ts b/frontend/src/app/routes/view/view.component.ts index 472a881..3725adb 100644 --- a/frontend/src/app/routes/view/view.component.ts +++ b/frontend/src/app/routes/view/view.component.ts @@ -18,6 +18,7 @@ export class ViewComponent implements OnInit { private utilService: UtilService ) {} + public previewLink = ''; public imageLinks = new ImageLinks(); async ngOnInit() { @@ -32,7 +33,16 @@ export class ViewComponent implements OnInit { return this.utilService.quitError(metadata.getReason()); } - this.imageLinks = this.imageService.CreateImageLinksFromID(id, 'qoi'); + const hasOriginal = metadata.fileMimes.original !== undefined; + this.previewLink = this.imageService.GetImageURL( + id, + metadata.fileMimes.master + ); + + this.imageLinks = this.imageService.CreateImageLinksFromID( + id, + hasOriginal ? null : metadata.fileMimes.master + ); } download() { diff --git a/frontend/src/app/services/api/image.service.ts b/frontend/src/app/services/api/image.service.ts index cbb97b1..76f5e85 100644 --- a/frontend/src/app/services/api/image.service.ts +++ b/frontend/src/app/services/api/image.service.ts @@ -1,7 +1,10 @@ import { Injectable } from '@angular/core'; -import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto'; +import { + ImageMetaResponse, + ImageUploadResponse +} from 'picsur-shared/dist/dto/api/image.dto'; import { ImageLinks } from 'picsur-shared/dist/dto/image-links.dto'; -import { EImage } from 'picsur-shared/dist/entities/image.entity'; +import { Mime2Ext } from 'picsur-shared/dist/dto/mimes.dto'; import { AsyncFailable } from 'picsur-shared/dist/types'; import { Open } from 'picsur-shared/dist/types/failable'; import { ImageUploadRequest } from '../../models/dto/image-upload-request.dto'; @@ -14,7 +17,7 @@ export class ImageService { public async UploadImage(image: File): AsyncFailable { const result = await this.api.postForm( - ImageMetaResponse, + ImageUploadResponse, '/i', new ImageUploadRequest(image) ); @@ -22,14 +25,15 @@ export class ImageService { return Open(result, 'id'); } - public async GetImageMeta(image: string): AsyncFailable { + public async GetImageMeta(image: string): AsyncFailable { return await this.api.get(ImageMetaResponse, `/i/meta/${image}`); } - public GetImageURL(image: string): string { + public GetImageURL(image: string, mime: string | null): string { const baseURL = window.location.protocol + '//' + window.location.host; + const extension = mime !== null ? Mime2Ext(mime) ?? 'error' : null; - return `${baseURL}/i/${image}`; + return `${baseURL}/i/${image}${extension !== null ? '.' + extension : ''}`; } public CreateImageLinks(imageURL: string): ImageLinks { @@ -42,7 +46,7 @@ export class ImageService { }; } - public CreateImageLinksFromID(imageID: string, format: string): ImageLinks { - return this.CreateImageLinks(this.GetImageURL(imageID + '.' + format)); + public CreateImageLinksFromID(imageID: string, mime: string | null): ImageLinks { + return this.CreateImageLinks(this.GetImageURL(imageID, mime)); } } diff --git a/shared/src/dto/api/image.dto.ts b/shared/src/dto/api/image.dto.ts index 7e373f1..6922273 100644 --- a/shared/src/dto/api/image.dto.ts +++ b/shared/src/dto/api/image.dto.ts @@ -1,5 +1,18 @@ +import { z } from 'zod'; import { EImageSchema } from '../../entities/image.entity'; import { createZodDto } from '../../util/create-zod-dto'; +import { ImageFileType } from '../image-file-types.dto'; -export const ImageMetaResponseSchema = EImageSchema; +export const ImageMetaResponseSchema = z.object({ + image: EImageSchema, + fileMimes: z.object({ + [ImageFileType.MASTER]: z.string(), + [ImageFileType.ORIGINAL]: z.union([z.string(), z.undefined()]), + }), +}); export class ImageMetaResponse extends createZodDto(ImageMetaResponseSchema) {} + +export const ImageUploadResponseSchema = EImageSchema; +export class ImageUploadResponse extends createZodDto( + ImageUploadResponseSchema, +) {} diff --git a/backend/src/models/constants/image-file-types.const.ts b/shared/src/dto/image-file-types.dto.ts similarity index 100% rename from backend/src/models/constants/image-file-types.const.ts rename to shared/src/dto/image-file-types.dto.ts