diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 1a8eecd..5f761f0 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AuthModule } from './routes/api/auth/auth.module'; -import { UserEntity } from './collections/userdb/user.entity'; import { ImageModule } from './routes/image/imageroute.module'; import { ServeStaticModule } from '@nestjs/serve-static'; import Config from './env'; -import { ImageEntity } from './collections/imagedb/image.entity'; import { DemoManagerModule } from './managers/demo/demomanager.module'; +import { EUser } from 'imagur-shared/dist/entities/user.entity'; +import { EImage } from 'imagur-shared/dist/entities/image.entity'; @Module({ imports: [ @@ -19,7 +19,7 @@ import { DemoManagerModule } from './managers/demo/demomanager.module'; database: Config.database.database, synchronize: true, - entities: [UserEntity, ImageEntity], + entities: [EUser, EImage], }), ServeStaticModule.forRoot({ rootPath: Config.static.frontendRoot, diff --git a/backend/src/backenddto/imageroute.dto.ts b/backend/src/backenddto/imageroute.dto.ts index 89176e4..50dec68 100644 --- a/backend/src/backenddto/imageroute.dto.ts +++ b/backend/src/backenddto/imageroute.dto.ts @@ -1,8 +1,10 @@ import { IsDefined, ValidateNested } from 'class-validator'; import { MultiPartFileDto } from './multipart.dto'; +import { Type } from 'class-transformer'; export class ImageUploadDto { @IsDefined() @ValidateNested() + @Type(() => MultiPartFileDto) image: MultiPartFileDto; } diff --git a/backend/src/collections/collectionutils.ts b/backend/src/collections/collectionutils.ts new file mode 100644 index 0000000..6675df1 --- /dev/null +++ b/backend/src/collections/collectionutils.ts @@ -0,0 +1,7 @@ +import { Repository } from 'typeorm'; + +export function GetCols(repository: Repository): (keyof T)[] { + return repository.metadata.columns.map( + (col) => col.propertyName, + ) as (keyof T)[]; +} diff --git a/backend/src/collections/imagedb/image.entity.ts b/backend/src/collections/imagedb/image.entity.ts deleted file mode 100644 index e7c4242..0000000 --- a/backend/src/collections/imagedb/image.entity.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; -import { SupportedMime, SupportedMimes } from './mimes.service'; - -@Entity() -export class ImageEntity { - @PrimaryGeneratedColumn() - id: number; - - @Index() - @Column({ unique: true }) - hash: string; - - // Binary data - @Column({ type: 'bytea', nullable: false }) - data: Buffer; - - @Column({ enum: SupportedMimes }) - mime: SupportedMime; -} diff --git a/backend/src/collections/imagedb/imagedb.module.ts b/backend/src/collections/imagedb/imagedb.module.ts index addcc98..2f722d8 100644 --- a/backend/src/collections/imagedb/imagedb.module.ts +++ b/backend/src/collections/imagedb/imagedb.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { ImageEntity } from './image.entity'; import { ImageDBService } from './imagedb.service'; import { MimesService } from './mimes.service'; +import { EImage } from 'imagur-shared/dist/entities/image.entity'; @Module({ - imports: [TypeOrmModule.forFeature([ImageEntity])], + imports: [TypeOrmModule.forFeature([EImage])], providers: [ImageDBService, MimesService], exports: [ImageDBService, MimesService], }) diff --git a/backend/src/collections/imagedb/imagedb.service.ts b/backend/src/collections/imagedb/imagedb.service.ts index c7680b8..f3c92d6 100644 --- a/backend/src/collections/imagedb/imagedb.service.ts +++ b/backend/src/collections/imagedb/imagedb.service.ts @@ -1,49 +1,55 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { ImageEntity } from './image.entity'; import Crypto from 'crypto'; -import { SupportedMime } from './mimes.service'; import { AsyncFailable, Fail, HasFailed, HasSuccess, } from 'imagur-shared/dist/types'; +import { SupportedMime } from 'imagur-shared/dist/dto/mimes.dto'; +import { GetCols } from '../collectionutils'; +import { EImage } from 'imagur-shared/dist/entities/image.entity'; @Injectable() export class ImageDBService { constructor( - @InjectRepository(ImageEntity) - private imageRepository: Repository, + @InjectRepository(EImage) + private imageRepository: Repository, ) {} public async create( image: Buffer, type: SupportedMime, - ): AsyncFailable { + ): AsyncFailable { const hash = this.hash(image); const find = await this.findOne(hash); if (HasSuccess(find)) return find; - const imageEntity = new ImageEntity(); + const imageEntity = new EImage(); imageEntity.data = image; imageEntity.mime = type; imageEntity.hash = hash; try { - await this.imageRepository.save(imageEntity); + return await this.imageRepository.save(imageEntity); } catch (e: any) { return Fail(e?.message); } - - return imageEntity; } - public async findOne(hash: string): AsyncFailable { + public async findOne( + hash: string, + getPrivate?: B, + ): AsyncFailable> { try { - const found = await this.imageRepository.findOne({ where: { hash } }); - if (found === undefined) return Fail('Image not found'); - return found; + const found = await this.imageRepository.findOne({ + where: { hash }, + select: getPrivate ? GetCols(this.imageRepository) : undefined, + }); + + if (!found) return Fail('Image not found'); + return found as B extends undefined ? EImage : Required; } catch (e: any) { return Fail(e?.message); } @@ -52,7 +58,7 @@ export class ImageDBService { public async findMany( startId: number, limit: number, - ): AsyncFailable { + ): AsyncFailable { try { const found = await this.imageRepository.find({ where: { id: { gte: startId } }, @@ -67,7 +73,6 @@ export class ImageDBService { public async delete(hash: string): AsyncFailable { const image = await this.findOne(hash); - if (HasFailed(image)) return image; try { diff --git a/backend/src/collections/imagedb/mimes.service.ts b/backend/src/collections/imagedb/mimes.service.ts index 307003d..de45730 100644 --- a/backend/src/collections/imagedb/mimes.service.ts +++ b/backend/src/collections/imagedb/mimes.service.ts @@ -1,39 +1,11 @@ import { Injectable } from '@nestjs/common'; import { Fail, Failable } from 'imagur-shared/dist/types'; - -const tuple = (...args: T): T => args; - -// Config - -const SupportedImageMimesTuple = tuple( - 'image/jpeg', - 'image/png', - 'image/webp', - 'image/tiff', - 'image/bmp', - 'image/x-icon', -); - -const SupportedAnimMimesTuple = tuple('image/apng', 'image/gif'); - -const SupportedMimesTuple = [ - ...SupportedImageMimesTuple, - ...SupportedAnimMimesTuple, -]; - -// Derivatives - -export const SupportedImageMimes: string[] = SupportedImageMimesTuple; -export const SupportedAnimMimes: string[] = SupportedAnimMimesTuple; - -export const SupportedMimes: string[] = SupportedMimesTuple; -export type SupportedMime = typeof SupportedMimesTuple[number]; -export type SupportedMimeCategory = 'image' | 'anim'; - -export interface FullMime { - mime: SupportedMime; - type: SupportedMimeCategory; -} +import { + FullMime, + SupportedAnimMimes, + SupportedImageMimes, + SupportedMime, +} from 'imagur-shared/dist/dto/mimes.dto'; @Injectable() export class MimesService { diff --git a/backend/src/collections/userdb/user.entity.ts b/backend/src/collections/userdb/user.entity.ts deleted file mode 100644 index 130ab76..0000000 --- a/backend/src/collections/userdb/user.entity.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; - -@Entity() -export class UserEntity { - @PrimaryGeneratedColumn() - id: number; - - @Index() - @Column({ unique: true }) - username: string; - - @Column() - password: string; - - @Column({ default: false }) - isAdmin: boolean; -} diff --git a/backend/src/collections/userdb/userdb.module.ts b/backend/src/collections/userdb/userdb.module.ts index 362bff6..38204a5 100644 --- a/backend/src/collections/userdb/userdb.module.ts +++ b/backend/src/collections/userdb/userdb.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { UserEntity } from './user.entity'; +import { EUser } from 'imagur-shared/dist/entities/user.entity'; import { UsersService } from './userdb.service'; @Module({ - imports: [TypeOrmModule.forFeature([UserEntity])], + imports: [TypeOrmModule.forFeature([EUser])], providers: [UsersService], exports: [UsersService], }) diff --git a/backend/src/collections/userdb/userdb.service.ts b/backend/src/collections/userdb/userdb.service.ts index 92937b5..382d51b 100644 --- a/backend/src/collections/userdb/userdb.service.ts +++ b/backend/src/collections/userdb/userdb.service.ts @@ -1,60 +1,71 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { AsyncFailable, Fail, HasFailed, HasSuccess } from 'imagur-shared/dist/types'; +import { validate } from 'class-validator'; +import { EUser } from 'imagur-shared/dist/entities/user.entity'; +import { + AsyncFailable, + Fail, + HasFailed, + HasSuccess, +} from 'imagur-shared/dist/types'; import { Repository } from 'typeorm'; -import { UserEntity } from './user.entity'; +import { GetCols } from '../collectionutils'; @Injectable() export class UsersService { + private readonly logger = new Logger('UsersService'); + constructor( - @InjectRepository(UserEntity) - private usersRepository: Repository, + @InjectRepository(EUser) + private usersRepository: Repository, ) {} public async create( username: string, hashedPassword: string, - ): AsyncFailable { + ): AsyncFailable { if (await this.exists(username)) return Fail('User already exists'); - const user = new UserEntity(); + const user = new EUser(); user.username = username; user.password = hashedPassword; try { - await this.usersRepository.save(user); + return await this.usersRepository.save(user); } catch (e: any) { return Fail(e?.message); } - - return user; } - public async delete(user: string | UserEntity): AsyncFailable { + public async delete(user: string | EUser): AsyncFailable { const userToModify = await this.resolve(user); - if (HasFailed(userToModify)) return userToModify; try { - await this.usersRepository.remove(userToModify); + return await this.usersRepository.remove(userToModify); } catch (e: any) { return Fail(e?.message); } - - return userToModify; } - public async findOne(username: string): AsyncFailable { + public async findOne( + username: string, + getPrivate?: B, + ): AsyncFailable> { try { - const found = await this.usersRepository.findOne({ where: { username } }); + const found = await this.usersRepository.findOne({ + where: { username }, + select: getPrivate ? GetCols(this.usersRepository) : undefined, + }); + if (!found) return Fail('User not found'); - return found; + return found as B extends undefined ? EUser : Required; } catch (e: any) { return Fail(e?.message); } } - public async findAll(): AsyncFailable { + public async findAll(): AsyncFailable { try { return await this.usersRepository.find(); } catch (e: any) { @@ -67,11 +78,10 @@ export class UsersService { } public async modifyAdmin( - user: string | UserEntity, + user: string | EUser, admin: boolean, ): AsyncFailable { const userToModify = await this.resolve(user); - if (HasFailed(userToModify)) return userToModify; userToModify.isAdmin = admin; @@ -80,12 +90,15 @@ export class UsersService { return true; } - private async resolve( - user: string | UserEntity, - ): AsyncFailable { + private async resolve(user: string | EUser): AsyncFailable { if (typeof user === 'string') { return await this.findOne(user); } else { + const errors = await validate(user); + if (errors.length > 0) { + this.logger.warn(errors); + return Fail('Invalid user'); + } return user; } } diff --git a/backend/src/managers/imagemanager/imagemanager.service.ts b/backend/src/managers/imagemanager/imagemanager.service.ts index 02d1858..4c58243 100644 --- a/backend/src/managers/imagemanager/imagemanager.service.ts +++ b/backend/src/managers/imagemanager/imagemanager.service.ts @@ -1,13 +1,11 @@ import { Injectable } from '@nestjs/common'; import { isHash } from 'class-validator'; import { fileTypeFromBuffer, FileTypeResult } from 'file-type'; +import { FullMime } from 'imagur-shared/dist/dto/mimes.dto'; +import { EImage } from 'imagur-shared/dist/entities/image.entity'; import { AsyncFailable, Fail, HasFailed } from 'imagur-shared/dist/types'; -import { ImageEntity } from '../../collections/imagedb/image.entity'; import { ImageDBService } from '../../collections/imagedb/imagedb.service'; -import { - MimesService, - FullMime, -} from '../../collections/imagedb/mimes.service'; +import { MimesService } from '../../collections/imagedb/mimes.service'; @Injectable() export class ImageManagerService { @@ -16,13 +14,16 @@ export class ImageManagerService { private readonly mimesService: MimesService, ) {} - public async retrieve(hash: string): AsyncFailable { - if (!isHash(hash, 'sha256')) return Fail('Invalid hash'); - + public async retrieveInfo(hash: string): AsyncFailable { return await this.imagesService.findOne(hash); } - public async upload(image: Buffer): AsyncFailable { + // Image data buffer is not included by default, this also returns that buffer + public async retrieveComplete(hash: string): AsyncFailable> { + return await this.imagesService.findOne(hash, true); + } + + public async upload(image: Buffer): AsyncFailable { const fullMime = await this.getFullMimeFromBuffer(image); if (HasFailed(fullMime)) return fullMime; @@ -34,7 +35,7 @@ export class ImageManagerService { ); if (HasFailed(imageEntity)) return imageEntity; - return imageEntity.hash; + return imageEntity; } private async process(image: Buffer, mime: FullMime): Promise { diff --git a/backend/src/routes/api/auth/admin.guard.ts b/backend/src/routes/api/auth/admin.guard.ts index 5637de7..9067c50 100644 --- a/backend/src/routes/api/auth/admin.guard.ts +++ b/backend/src/routes/api/auth/admin.guard.ts @@ -6,7 +6,7 @@ import { } from '@nestjs/common'; import { plainToClass } from 'class-transformer'; import { validate } from 'class-validator'; -import { User } from 'imagur-shared/dist/dto/user.dto'; +import { EUser } from 'imagur-shared/dist/entities/user.entity'; @Injectable() export class AdminGuard implements CanActivate { @@ -15,7 +15,7 @@ export class AdminGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); - const user = plainToClass(User, request.user); + const user = plainToClass(EUser, request.user); const errors = await validate(user, {forbidUnknownValues: true}); if (errors.length > 0) { this.logger.warn(errors); diff --git a/backend/src/routes/api/auth/auth.controller.ts b/backend/src/routes/api/auth/auth.controller.ts index fa76758..a709101 100644 --- a/backend/src/routes/api/auth/auth.controller.ts +++ b/backend/src/routes/api/auth/auth.controller.ts @@ -48,7 +48,7 @@ export class AuthController { await this.authService.makeAdmin(user); } - return this.authService.userEntityToUser(user); + return user; } @UseGuards(JwtAuthGuard, AdminGuard) @@ -60,7 +60,7 @@ export class AuthController { const user = await this.authService.deleteUser(deleteData.username); if (HasFailed(user)) throw new NotFoundException('User does not exist'); - return this.authService.userEntityToUser(user); + return user; } @UseGuards(JwtAuthGuard, AdminGuard) diff --git a/backend/src/routes/api/auth/auth.service.ts b/backend/src/routes/api/auth/auth.service.ts index 3c8d257..6bc9569 100644 --- a/backend/src/routes/api/auth/auth.service.ts +++ b/backend/src/routes/api/auth/auth.service.ts @@ -2,9 +2,8 @@ import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import { JwtDataDto } from 'imagur-shared/dist/dto/auth.dto'; -import { User } from 'imagur-shared/dist/dto/user.dto'; +import { EUser } from 'imagur-shared/dist/entities/user.entity'; import { AsyncFailable, HasFailed, Fail } from 'imagur-shared/dist/types'; -import { UserEntity } from '../../../collections/userdb/user.entity'; import { UsersService } from '../../../collections/userdb/userdb.service'; @Injectable() @@ -14,62 +13,42 @@ export class AuthService { private jwtService: JwtService, ) {} - async createUser( - username: string, - password: string, - ): AsyncFailable { + async createUser(username: string, password: string): AsyncFailable { const hashedPassword = await bcrypt.hash(password, 12); return this.usersService.create(username, hashedPassword); } - async deleteUser(user: string | UserEntity): AsyncFailable { + async deleteUser(user: string | EUser): AsyncFailable { return this.usersService.delete(user); } - async listUsers(): AsyncFailable { - const users = await this.usersService.findAll(); - if (HasFailed(users)) return users; - - return users.map((user) => this.userEntityToUser(user)); + async listUsers(): AsyncFailable { + return this.usersService.findAll(); } - async authenticate( - username: string, - password: string, - ): AsyncFailable { - const user = await this.usersService.findOne(username); - + async authenticate(username: string, password: string): AsyncFailable { + const user = await this.usersService.findOne(username, true); if (HasFailed(user)) return user; if (!(await bcrypt.compare(password, user.password))) return Fail('Wrong password'); - return user; + return await this.usersService.findOne(username); } - async createToken(user: User): Promise { + async createToken(user: EUser): Promise { const jwtData: JwtDataDto = { - user: { - username: user.username, - isAdmin: user.isAdmin, - }, + user, }; return this.jwtService.signAsync(jwtData); } - async makeAdmin(user: string | UserEntity): AsyncFailable { + async makeAdmin(user: string | EUser): AsyncFailable { return this.usersService.modifyAdmin(user, true); } - async revokeAdmin(user: string | UserEntity): AsyncFailable { + async revokeAdmin(user: string | EUser): AsyncFailable { return this.usersService.modifyAdmin(user, false); } - - userEntityToUser(user: UserEntity): User { - return { - username: user.username, - isAdmin: user.isAdmin, - }; - } } diff --git a/backend/src/routes/api/auth/authrequest.ts b/backend/src/routes/api/auth/authrequest.ts index d8c792d..78e0718 100644 --- a/backend/src/routes/api/auth/authrequest.ts +++ b/backend/src/routes/api/auth/authrequest.ts @@ -1,6 +1,6 @@ import { FastifyRequest } from 'fastify'; -import { User } from 'imagur-shared/dist/dto/user.dto'; +import { EUser } from 'imagur-shared/dist/entities/user.entity'; export default interface AuthFasityRequest extends FastifyRequest { - user: User; + user: EUser; } diff --git a/backend/src/routes/api/auth/jwt.strategy.ts b/backend/src/routes/api/auth/jwt.strategy.ts index e31baf8..f78b042 100644 --- a/backend/src/routes/api/auth/jwt.strategy.ts +++ b/backend/src/routes/api/auth/jwt.strategy.ts @@ -5,7 +5,7 @@ import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; import Config from '../../../env'; import { JwtDataDto } from 'imagur-shared/dist/dto/auth.dto'; -import { User } from 'imagur-shared/dist/dto/user.dto'; +import { EUser } from 'imagur-shared/dist/entities/user.entity'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { @@ -19,10 +19,10 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { }); } - async validate(payload: any): Promise { + async validate(payload: any): Promise { const jwt = plainToClass(JwtDataDto, payload); - const errors = await validate(jwt); + const errors = await validate(jwt, { forbidUnknownValues: true }); if (errors.length > 0) { this.logger.warn(errors); throw new UnauthorizedException(); diff --git a/backend/src/routes/api/auth/local.strategy.ts b/backend/src/routes/api/auth/local.strategy.ts index c6885c6..882d532 100644 --- a/backend/src/routes/api/auth/local.strategy.ts +++ b/backend/src/routes/api/auth/local.strategy.ts @@ -3,7 +3,7 @@ import { PassportStrategy } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; import { AsyncFailable, HasFailed } from 'imagur-shared/dist/types'; -import { User } from 'imagur-shared/dist/dto/user.dto'; +import { EUser } from 'imagur-shared/dist/entities/user.entity'; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy, 'local') { @@ -11,15 +11,11 @@ export class LocalStrategy extends PassportStrategy(Strategy, 'local') { super(); } - async validate(username: string, password: string): AsyncFailable { - const userEntity = await this.authService.authenticate(username, password); - if (HasFailed(userEntity)) { + async validate(username: string, password: string): AsyncFailable { + const user = await this.authService.authenticate(username, password); + if (HasFailed(user)) { throw new UnauthorizedException(); } - const user: User = { - username: userEntity.username, - isAdmin: userEntity.isAdmin, - }; return user; } diff --git a/backend/src/routes/image/imageroute.controller.ts b/backend/src/routes/image/imageroute.controller.ts index e512ce2..5525cde 100644 --- a/backend/src/routes/image/imageroute.controller.ts +++ b/backend/src/routes/image/imageroute.controller.ts @@ -26,7 +26,7 @@ export class ImageController { ) { if (!isHash(hash, 'sha256')) throw new BadRequestException('Invalid hash'); - const image = await this.imagesService.retrieve(hash); + const image = await this.imagesService.retrieveComplete(hash); if (HasFailed(image)) throw new NotFoundException('Failed to retrieve image'); @@ -34,17 +34,28 @@ export class ImageController { return image.data; } + @Get('meta/:hash') + async getImageMeta(@Param('hash') hash: string) { + if (!isHash(hash, 'sha256')) throw new BadRequestException('Invalid hash'); + + const image = await this.imagesService.retrieveInfo(hash); + if (HasFailed(image)) + throw new NotFoundException('Failed to retrieve image'); + + return image; + } + @Post() async uploadImage( @Req() req: FastifyRequest, @MultiPart(ImageUploadDto) multipart: ImageUploadDto, ) { const fileBuffer = await multipart.image.toBuffer(); - const hash = await this.imagesService.upload(fileBuffer); - if (HasFailed(hash)) { + const image = await this.imagesService.upload(fileBuffer); + if (HasFailed(image)) { throw new InternalServerErrorException('Failed to upload image'); } - return { hash }; + return image; } } diff --git a/frontend/craco.config.js b/frontend/craco.config.js index b002fb7..14045c4 100644 --- a/frontend/craco.config.js +++ b/frontend/craco.config.js @@ -1,7 +1,15 @@ const scopedcss = require('craco-plugin-scoped-css'); const path = require('path'); +const webpack = require('webpack'); module.exports = { + webpack: { + plugins: { + add: [ + new webpack.IgnorePlugin({resourceRegExp: /react-native-sqlite-storage/}), + ] + } + }, devServer: { devMiddleware: { writeToDisk: true, diff --git a/frontend/src/api/images.ts b/frontend/src/api/images.ts index 8196d0c..e5d4497 100644 --- a/frontend/src/api/images.ts +++ b/frontend/src/api/images.ts @@ -1,7 +1,7 @@ -import { AsyncFailable, HasFailed } from 'imagur-shared/dist/types'; -import { ImageUploadResponse } from 'imagur-shared/dist/dto/images.dto'; import { ImageUploadRequest } from '../frontenddto/imageroute.dto'; import ImagurApi from './api'; +import { EImage } from 'imagur-shared/dist/entities/image.entity'; +import { AsyncFailable, HasFailed } from 'imagur-shared/dist/types'; export interface ImageLinks { source: string; @@ -19,7 +19,7 @@ export default class ImagesApi extends ImagurApi { public async UploadImage(image: File): AsyncFailable { const result = await this.api.postForm( - ImageUploadResponse, + EImage, '/i', new ImageUploadRequest(image), ); diff --git a/shared/package.json b/shared/package.json index 4a0c6c1..c7fdc0d 100644 --- a/shared/package.json +++ b/shared/package.json @@ -9,8 +9,10 @@ "type": "commonjs", "main": "./dist/index.js", "dependencies": { + "class-transformer": "^0.5.1", "class-validator": "^0.13.2", - "tsc-watch": "^4.6.0" + "tsc-watch": "^4.6.0", + "typeorm": "^0.2.44" }, "devDependencies": { "@types/node": "^17.0.21", diff --git a/shared/src/dto/api.dto.ts b/shared/src/dto/api.dto.ts index f40dd35..455b595 100644 --- a/shared/src/dto/api.dto.ts +++ b/shared/src/dto/api.dto.ts @@ -1,5 +1,4 @@ import { - Equals, IsBoolean, IsDefined, IsInt, diff --git a/shared/src/dto/auth.dto.ts b/shared/src/dto/auth.dto.ts index 720b09a..4112d82 100644 --- a/shared/src/dto/auth.dto.ts +++ b/shared/src/dto/auth.dto.ts @@ -7,7 +7,8 @@ import { IsInt, ValidateNested, } from 'class-validator'; -import { User } from './user.dto'; +import { EUser } from '../entities/user.entity'; +import { Type } from 'class-transformer'; // Api @@ -47,14 +48,15 @@ export class AuthDeleteRequest { username: string; } -export class AuthDeleteResponse extends User {} +export class AuthDeleteResponse extends EUser {} // Extra export class JwtDataDto { - @ValidateNested() @IsDefined() - user: User; + @ValidateNested() + @Type(() => EUser) + user: EUser; @IsOptional() @IsInt() diff --git a/shared/src/dto/images.dto.ts b/shared/src/dto/images.dto.ts deleted file mode 100644 index a2c4209..0000000 --- a/shared/src/dto/images.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IsHash, IsString } from 'class-validator'; - -export class ImageUploadResponse { - @IsString() - @IsHash('sha256') - hash: string; -} diff --git a/shared/src/dto/mimes.dto.ts b/shared/src/dto/mimes.dto.ts new file mode 100644 index 0000000..7d6e05e --- /dev/null +++ b/shared/src/dto/mimes.dto.ts @@ -0,0 +1,33 @@ +const tuple = (...args: T): T => args; + +// Config + +const SupportedImageMimesTuple = tuple( + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/tiff', + 'image/bmp', + 'image/x-icon', +); + +const SupportedAnimMimesTuple = tuple('image/apng', 'image/gif'); + +const SupportedMimesTuple = [ + ...SupportedImageMimesTuple, + ...SupportedAnimMimesTuple, +]; + +// Derivatives + +export const SupportedImageMimes: string[] = SupportedImageMimesTuple; +export const SupportedAnimMimes: string[] = SupportedAnimMimesTuple; + +export const SupportedMimes: string[] = SupportedMimesTuple; +export type SupportedMime = typeof SupportedMimesTuple[number]; +export type SupportedMimeCategory = 'image' | 'anim'; + +export interface FullMime { + mime: SupportedMime; + type: SupportedMimeCategory; +} diff --git a/shared/src/dto/user.dto.ts b/shared/src/dto/user.dto.ts deleted file mode 100644 index 7b7a1fe..0000000 --- a/shared/src/dto/user.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IsBoolean, IsDefined, IsNotEmpty, IsString } from 'class-validator'; - -export class User { - @IsString() - @IsNotEmpty() - username: string; - - @IsDefined() - @IsBoolean() - isAdmin: boolean; -} diff --git a/shared/src/entities/image.entity.ts b/shared/src/entities/image.entity.ts new file mode 100644 index 0000000..5e21cdf --- /dev/null +++ b/shared/src/entities/image.entity.ts @@ -0,0 +1,27 @@ +import { IsDefined, IsEnum, IsHash, IsNumber, IsOptional, IsString } from 'class-validator'; +import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; +import { SupportedMime, SupportedMimes } from '../dto/mimes.dto'; + +@Entity() +export class EImage { + @PrimaryGeneratedColumn() + @IsNumber() + @IsDefined() + id: number; + + @Index() + @Column({ unique: true }) + @IsString() + @IsHash('sha256') + hash: string; + + // Binary data + @Column({ type: 'bytea', nullable: false, select: false }) + @IsOptional() + data?: Buffer; + + @Column({ enum: SupportedMimes }) + @IsEnum(SupportedMimes) + @IsDefined() + mime: SupportedMime; +} diff --git a/shared/src/entities/user.entity.ts b/shared/src/entities/user.entity.ts new file mode 100644 index 0000000..c3addc8 --- /dev/null +++ b/shared/src/entities/user.entity.ts @@ -0,0 +1,33 @@ +import { + IsBoolean, + IsDefined, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, +} from 'class-validator'; +import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity() +export class EUser { + @PrimaryGeneratedColumn() + @IsNumber() + @IsDefined() + id: number; + + @Index() + @Column({ unique: true }) + @IsString() + @IsNotEmpty() + username: string; + + @Column({ default: false }) + @IsDefined() + @IsBoolean() + isAdmin: boolean; + + @Column({ select: false }) + @IsOptional() + @IsString() + password?: string; +}