From 626e22899177ebe384faedb7331112492688ef42 Mon Sep 17 00:00:00 2001 From: rubikscraft Date: Fri, 25 Feb 2022 12:22:00 +0100 Subject: [PATCH] better shared api calls --- .vscode/tasks.json | 11 +- backend/src/backenddto/imageroute.dto.ts | 8 ++ .../multipart.dto.ts | 0 backend/src/decorators/multipart.decorator.ts | 6 +- .../http-exception/http-exception.filter.ts | 9 +- .../src/layers/success/success.interceptor.ts | 8 +- .../imagemanager/imagemanager.service.ts | 12 +- backend/src/routes/api/auth/admin.guard.ts | 2 +- .../src/routes/api/auth/auth.controller.ts | 12 +- backend/src/routes/api/auth/auth.service.ts | 4 +- backend/src/routes/api/auth/authrequest.ts | 2 +- backend/src/routes/api/auth/jwt.strategy.ts | 4 +- backend/src/routes/api/auth/local.strategy.ts | 2 +- .../src/routes/image/imageroute.controller.ts | 6 +- backend/src/routes/image/imageroute.dto.ts | 9 -- branding/logo/imagur-128.png | Bin 0 -> 1627 bytes branding/logo/imagur-512.png | Bin 0 -> 7068 bytes branding/logo/imagur.ico | Bin 0 -> 1150 bytes branding/logo/imagur.svg | 61 ++++++++ frontend/package.json | 6 +- frontend/public/favicon.ico | Bin 3870 -> 0 bytes frontend/public/image/logo | 1 + frontend/public/index.html | 2 +- frontend/src/api/api.dto.ts | 14 -- frontend/src/api/api.ts | 133 ++++++++++++++++++ frontend/src/api/images.ts | 66 ++++++--- frontend/src/api/util.ts | 31 ---- frontend/src/frontenddto/imageroute.dto.ts | 11 ++ frontend/src/lib/debounce.ts | 13 ++ frontend/src/routes/processing/processing.tsx | 4 +- frontend/src/routes/view/view.tsx | 11 +- shared/package.json | 3 +- shared/src/dto/api.dto.ts | 45 ++++++ .../api/auth => shared/src/dto}/auth.dto.ts | 33 ++++- shared/src/dto/images.dto.ts | 7 + .../userdb => shared/src/dto}/user.dto.ts | 0 shared/tsconfig.json | 2 +- 37 files changed, 407 insertions(+), 131 deletions(-) create mode 100644 backend/src/backenddto/imageroute.dto.ts rename backend/src/{decorators => backenddto}/multipart.dto.ts (100%) delete mode 100644 backend/src/routes/image/imageroute.dto.ts create mode 100644 branding/logo/imagur-128.png create mode 100644 branding/logo/imagur-512.png create mode 100644 branding/logo/imagur.ico create mode 100644 branding/logo/imagur.svg delete mode 100644 frontend/public/favicon.ico create mode 120000 frontend/public/image/logo delete mode 100644 frontend/src/api/api.dto.ts create mode 100644 frontend/src/api/api.ts delete mode 100644 frontend/src/api/util.ts create mode 100644 frontend/src/frontenddto/imageroute.dto.ts create mode 100644 frontend/src/lib/debounce.ts create mode 100644 shared/src/dto/api.dto.ts rename {backend/src/routes/api/auth => shared/src/dto}/auth.dto.ts (51%) create mode 100644 shared/src/dto/images.dto.ts rename {backend/src/collections/userdb => shared/src/dto}/user.dto.ts (100%) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b9a4f10..f363c4c 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -3,7 +3,12 @@ "tasks": [ { "label": "Start full", - "dependsOn": ["Start backend", "Start frontend", "Start postgres"], + "dependsOn": [ + "Start backend", + "Start frontend", + "Start postgres", + "Start shared" + ], "dependsOrder": "parallel", "isBackground": true, "group": "build" @@ -24,7 +29,6 @@ "options": { "cwd": "./backend" }, - "dependsOn": ["Start shared"], "group": "build" }, { @@ -34,13 +38,12 @@ "options": { "cwd": "./frontend" }, - "dependsOn": ["Start shared"], "group": "build" }, { "type": "shell", "label": "Start postgres", - "command": "podman-compose up", + "command": "podman-compose stop; podman-compose up", "options": { "cwd": "./support" }, diff --git a/backend/src/backenddto/imageroute.dto.ts b/backend/src/backenddto/imageroute.dto.ts new file mode 100644 index 0000000..89176e4 --- /dev/null +++ b/backend/src/backenddto/imageroute.dto.ts @@ -0,0 +1,8 @@ +import { IsDefined, ValidateNested } from 'class-validator'; +import { MultiPartFileDto } from './multipart.dto'; + +export class ImageUploadDto { + @IsDefined() + @ValidateNested() + image: MultiPartFileDto; +} diff --git a/backend/src/decorators/multipart.dto.ts b/backend/src/backenddto/multipart.dto.ts similarity index 100% rename from backend/src/decorators/multipart.dto.ts rename to backend/src/backenddto/multipart.dto.ts diff --git a/backend/src/decorators/multipart.decorator.ts b/backend/src/decorators/multipart.decorator.ts index c6632b4..9feb7d4 100644 --- a/backend/src/decorators/multipart.decorator.ts +++ b/backend/src/decorators/multipart.decorator.ts @@ -10,7 +10,7 @@ import { FastifyRequest } from 'fastify'; import { Multipart, MultipartFields, MultipartFile } from 'fastify-multipart'; import { Newable } from 'imagur-shared/dist/types'; import Config from '../env'; -import { MultiPartFieldDto, MultiPartFileDto } from './multipart.dto'; +import { MultiPartFieldDto, MultiPartFileDto } from '../backenddto/multipart.dto'; const logger = new Logger('MultiPart'); export interface MPFile { @@ -47,10 +47,8 @@ export const PostFile = createParamDecorator( }, ); -export class MultiPartDto {} - export const MultiPart = createParamDecorator( - async (data: Newable, ctx: ExecutionContext) => { + async (data: Newable, ctx: ExecutionContext) => { const req: FastifyRequest = ctx.switchToHttp().getRequest(); const dtoClass = new data(); diff --git a/backend/src/layers/http-exception/http-exception.filter.ts b/backend/src/layers/http-exception/http-exception.filter.ts index 9d92a88..a5e09c1 100644 --- a/backend/src/layers/http-exception/http-exception.filter.ts +++ b/backend/src/layers/http-exception/http-exception.filter.ts @@ -6,6 +6,7 @@ import { } from '@nestjs/common'; import { FastifyRequest } from 'fastify'; import { FastifyReply } from 'fastify'; +import { ApiErrorResponse, ApiResponse } from 'imagur-shared/dist/dto/api.dto'; @Catch(HttpException) export class MainExceptionFilter implements ExceptionFilter { @@ -15,14 +16,16 @@ export class MainExceptionFilter implements ExceptionFilter { const request = ctx.getRequest(); const status = exception.getStatus(); - response.status(status).send({ - success: status < 400, + const toSend: ApiErrorResponse = { + success: false, statusCode: status, timestamp: new Date().toISOString(), data: { message: exception.message, }, - }); + }; + + response.status(status).send(toSend); } } diff --git a/backend/src/layers/success/success.interceptor.ts b/backend/src/layers/success/success.interceptor.ts index 155e71a..f1061c1 100644 --- a/backend/src/layers/success/success.interceptor.ts +++ b/backend/src/layers/success/success.interceptor.ts @@ -4,7 +4,7 @@ import { ExecutionContext, CallHandler, } from '@nestjs/common'; -import { FastifyReply } from 'fastify'; +import { ApiResponse } from 'imagur-shared/dist/dto/api.dto'; import { Observable, map } from 'rxjs'; @Injectable() @@ -16,13 +16,15 @@ export class SuccessInterceptor implements NestInterceptor { return data; } else if (typeof data === 'object') { const status = context.switchToHttp().getResponse().statusCode; - return { - success: status < 400, + const response: ApiResponse = { + success: true, statusCode: status, timestamp: new Date().toISOString(), data, }; + + return response; } else { return data; } diff --git a/backend/src/managers/imagemanager/imagemanager.service.ts b/backend/src/managers/imagemanager/imagemanager.service.ts index d38e0f3..02d1858 100644 --- a/backend/src/managers/imagemanager/imagemanager.service.ts +++ b/backend/src/managers/imagemanager/imagemanager.service.ts @@ -1,9 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { isHash } from 'class-validator'; import { fileTypeFromBuffer, FileTypeResult } from 'file-type'; 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, + FullMime, +} from '../../collections/imagedb/mimes.service'; @Injectable() export class ImageManagerService { @@ -13,7 +17,7 @@ export class ImageManagerService { ) {} public async retrieve(hash: string): AsyncFailable { - if (!this.validateHash(hash)) return Fail('Invalid hash'); + if (!isHash(hash, 'sha256')) return Fail('Invalid hash'); return await this.imagesService.findOne(hash); } @@ -44,8 +48,4 @@ export class ImageManagerService { ); return fullMime; } - - public validateHash(hash: string): boolean { - return /^[a-f0-9]{64}$/.test(hash); - } } diff --git a/backend/src/routes/api/auth/admin.guard.ts b/backend/src/routes/api/auth/admin.guard.ts index e3822ec..5637de7 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 '../../../collections/userdb/user.dto'; +import { User } from 'imagur-shared/dist/dto/user.dto'; @Injectable() export class AdminGuard implements CanActivate { diff --git a/backend/src/routes/api/auth/auth.controller.ts b/backend/src/routes/api/auth/auth.controller.ts index dd48dc5..fa76758 100644 --- a/backend/src/routes/api/auth/auth.controller.ts +++ b/backend/src/routes/api/auth/auth.controller.ts @@ -10,16 +10,12 @@ import { InternalServerErrorException, } from '@nestjs/common'; import { LocalAuthGuard } from './local-auth.guard'; -import { - RegisterRequestDto, - LoginResponseDto, - DeleteRequestDto, -} from './auth.dto'; import { AuthService } from './auth.service'; import { JwtAuthGuard } from './jwt.guard'; import { AdminGuard } from './admin.guard'; import { HasFailed } from 'imagur-shared/dist/types'; import AuthFasityRequest from './authrequest'; +import { AuthDeleteRequest, AuthLoginResponse, AuthRegisterRequest } from 'imagur-shared/dist/dto/auth.dto'; @Controller('api/auth') export class AuthController { @@ -28,7 +24,7 @@ export class AuthController { @UseGuards(LocalAuthGuard) @Post('login') async login(@Request() req: AuthFasityRequest) { - const response: LoginResponseDto = { + const response: AuthLoginResponse = { access_token: await this.authService.createToken(req.user), }; @@ -39,7 +35,7 @@ export class AuthController { @Post('create') async register( @Request() req: AuthFasityRequest, - @Body() register: RegisterRequestDto, + @Body() register: AuthRegisterRequest, ) { const user = await this.authService.createUser( register.username, @@ -59,7 +55,7 @@ export class AuthController { @Post('delete') async delete( @Request() req: AuthFasityRequest, - @Body() deleteData: DeleteRequestDto, + @Body() deleteData: AuthDeleteRequest, ) { const user = await this.authService.deleteUser(deleteData.username); if (HasFailed(user)) throw new NotFoundException('User does not exist'); diff --git a/backend/src/routes/api/auth/auth.service.ts b/backend/src/routes/api/auth/auth.service.ts index e428a0d..3c8d257 100644 --- a/backend/src/routes/api/auth/auth.service.ts +++ b/backend/src/routes/api/auth/auth.service.ts @@ -1,11 +1,11 @@ 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 { AsyncFailable, HasFailed, Fail } from 'imagur-shared/dist/types'; -import { User } from '../../../collections/userdb/user.dto'; import { UserEntity } from '../../../collections/userdb/user.entity'; import { UsersService } from '../../../collections/userdb/userdb.service'; -import { JwtDataDto } from './auth.dto'; @Injectable() export class AuthService { diff --git a/backend/src/routes/api/auth/authrequest.ts b/backend/src/routes/api/auth/authrequest.ts index b278456..d8c792d 100644 --- a/backend/src/routes/api/auth/authrequest.ts +++ b/backend/src/routes/api/auth/authrequest.ts @@ -1,5 +1,5 @@ import { FastifyRequest } from 'fastify'; -import { User } from '../../../collections/userdb/user.dto'; +import { User } from 'imagur-shared/dist/dto/user.dto'; export default interface AuthFasityRequest extends FastifyRequest { user: User; diff --git a/backend/src/routes/api/auth/jwt.strategy.ts b/backend/src/routes/api/auth/jwt.strategy.ts index 722e404..e31baf8 100644 --- a/backend/src/routes/api/auth/jwt.strategy.ts +++ b/backend/src/routes/api/auth/jwt.strategy.ts @@ -2,10 +2,10 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; import { validate } from 'class-validator'; -import { JwtDataDto } from './auth.dto'; import { plainToClass } from 'class-transformer'; -import { User } from '../../../collections/userdb/user.dto'; import Config from '../../../env'; +import { JwtDataDto } from 'imagur-shared/dist/dto/auth.dto'; +import { User } from 'imagur-shared/dist/dto/user.dto'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { diff --git a/backend/src/routes/api/auth/local.strategy.ts b/backend/src/routes/api/auth/local.strategy.ts index 1687911..c6885c6 100644 --- a/backend/src/routes/api/auth/local.strategy.ts +++ b/backend/src/routes/api/auth/local.strategy.ts @@ -2,8 +2,8 @@ import { Strategy } from 'passport-local'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; -import { User } from '../../../collections/userdb/user.dto'; import { AsyncFailable, HasFailed } from 'imagur-shared/dist/types'; +import { User } from 'imagur-shared/dist/dto/user.dto'; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy, 'local') { diff --git a/backend/src/routes/image/imageroute.controller.ts b/backend/src/routes/image/imageroute.controller.ts index e0183b4..e512ce2 100644 --- a/backend/src/routes/image/imageroute.controller.ts +++ b/backend/src/routes/image/imageroute.controller.ts @@ -13,7 +13,8 @@ import { FastifyReply, FastifyRequest } from 'fastify'; import { HasFailed } from 'imagur-shared/dist/types'; import { MultiPart } from '../../decorators/multipart.decorator'; import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service'; -import { ImageUploadDto } from './imageroute.dto'; +import { ImageUploadDto } from '../../backenddto/imageroute.dto'; +import { isHash } from 'class-validator'; @Controller('i') export class ImageController { constructor(private readonly imagesService: ImageManagerService) {} @@ -23,8 +24,7 @@ export class ImageController { @Res({ passthrough: true }) res: FastifyReply, @Param('hash') hash: string, ) { - if (!this.imagesService.validateHash(hash)) - throw new BadRequestException('Invalid hash'); + if (!isHash(hash, 'sha256')) throw new BadRequestException('Invalid hash'); const image = await this.imagesService.retrieve(hash); if (HasFailed(image)) diff --git a/backend/src/routes/image/imageroute.dto.ts b/backend/src/routes/image/imageroute.dto.ts deleted file mode 100644 index 2c87e89..0000000 --- a/backend/src/routes/image/imageroute.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IsDefined, ValidateNested } from 'class-validator'; -import { MultiPartDto } from '../../decorators/multipart.decorator'; -import { MultiPartFileDto } from '../../decorators/multipart.dto'; - -export class ImageUploadDto extends MultiPartDto { - @IsDefined() - @ValidateNested() - image: MultiPartFileDto; -} diff --git a/branding/logo/imagur-128.png b/branding/logo/imagur-128.png new file mode 100644 index 0000000000000000000000000000000000000000..1c613ac894bc906985d1fddb8399b6aac141bb0f GIT binary patch literal 1627 zcmcJQdpHw%7{`Cx+A&io5+f$jle9(;Nv@-8osuN?;~H{lb6Hc7&1DCvX5FZq$2C!w zoe*-_+@kDIol*Gt}8sx(s0PF^~|dX0QsESwy&%r)d{Z0Q7E(dZ>bf+s!%Wf&>riKe>)%)nHLqmc<~0 z3=Il3aP~2RgtI$DO^m_z%VwfCpQ+^Hkie1KjLBmDm5 zZ}7{NML0tcZ%Lmx>oO$$Mn<{4>@~AN7DGF`L#r#8cYDJ8^o|un%}q)^_pRBQq?)!k z7}BtmzS23x8%T8e%4$c3cl2CDYKALthG`3J-lCSX5h^Uuwq^jq?)v}bXb85p&_AzO zvmVdhMCSLuqsFFT-yu}Wz_6FM1of7vZ*NFzv{~Z64hkk@<=3Z!Y!k<4hGT zINI}}o6%coQ{>mRUj1g&_%MaW#wX9jC(&C0D5v_p`HC6)kq{|mYfHz{nF{%J&oc)r z`nXfX)eG4ZBdwiq03!VOxLK#D6K;!>nE8a*L+p#Ksb|27lg=I;U;B8$mwI}9#PMO# zCyV8gMBr_ffaja{vNs)TGENbmlB@4ZtE{@_?Z+zo+-o6P#P&h}P?g~l@aqz_0|%a&|HMY&SYV{r{`$b4mHN=g>?701ew2 z!*{Jy0#$5{Hro>mXwqru)5U{r<=~qGX~VF9EP$sg>~mu;=X*7Zqx7Gx|%iv}$1Y zLv?D57iL`j7k`(p145PEs(Bc=bxo}Y?jP7onF{{G+tIdR>gBXI+1De~wf4F|)WcxY zbyuH>r+1lP=XER#lU;$BzT;N!!dH%CS^Ta$F6&v?QLB_FsbKat9m&DNvpQ#}@rps~ z+;H)9Wm_+(x>w_!)rzR|-c^bUH<8?kPc}ZYrk4LkfVFhzyXl~A`DOzbh&g9=%_MYX z3v1ui8_4`q=c|3&WeCcKyYa0&jif6|-?MQt@+r|Ow-ss#dCEvE zVhr*)vOp?C#5`1xH0jm$nayNtMXom3%|9BG1gLOZc~9Z{Ni96SLV=+6i-l<%w=gq2d`RdJ0N}8(k)9O* zjPQ~X9AtsN$YDe({Ke*PbmJ}ntT*XD45`gk@8C_rAbr~)Yd?>m(3^p`Kxk;F{B2+F zyKXoAZ^`=wA~TmXg#ZwiG1k+y3Cmg>8F=P?J92Dd>~nE(-Rs)dHANeBNL!UZ0mFHT zryq}>Vt@PgAlrlF>9-39X^0vI;}eeLH zrIp`5);=kt{Y$Z$(%7%+rP;l2?lw5{Y|U^pMGcK14scvHkH`dKcBKF~ehE(K{Hg!G z3*{6P=t88Z}Z1QC-#bgb`eWVitS7|%d1TzWJP~|JXA8yESsgU&0+!`A+ z)kCg35I@8WIG%h7I-M_X*0wnpFM!bEo05{u)x0*^GgiIrDW&S?M_A7;v2Cako4?+h zYgrQNsrb~2zVcTPjGm5`XIv+D+(qtrspdN?iAuy7*6#VK&Uf_J)CpppY~u~b?BYvm zGaYGau}eol7m{FxX&WBmY-`7Mwsg6lo=YP&d!0pWoJ*@;KNpe~`lm00odm#hr93uK z+)k@jCzJ0biHe&ee&yU&%`97=^wQj%yjWbnDcw|ih^)x~5XH(?m}c|NwjB*@OP9^y z*t&B`iAH7sjh(%0XUA1pc+?4hoej+D>Q_IhelM2bwyfQ)#=6&bN5Q9Uq@hMscFZBr z0HB_N7_qzK6xOL0g=|c6bu!WV57))-Uc=NW;j+57si;FMOw52xUnR4s%`0%Sl9UvT zH&JMHoSi*jV?d*O_|D%rriUN~=f6^zp-xFIS5#K)e?Qaj)Q_3ZsXLnaUe(Gxc7Y8T zy;62au%@m1wC@oeNHW{`mfkHR4OOBWv?ZK3)A17wAf7{91hYn4jTg`uh;W`C;=Etz|hH$tV_%v#_QA!Y}g{B2P~tl3cg7A%dRk6E#9GvHS^QI5X8X z1lN|eo}&ml(g`QcqR9?64qQM~3g90kyf|q_`%*1!qE)+;^2Pl$CVJL}BzVrB83c8e z&jDDZBsa7y@QSc8Vpc9w1*>&65 zem)n*j{0x;*aO)o0iG!zmlaY;)l&amimBE;67&R)Mx9zeRVd~D0!HmtsF*Pv<9!f= zc#aAjv|fPQl5=6wJGP5~5vSBvo~C{`l9`F3`jZ5^HkiS}$Ju1d(4EWUT^civ z{%jGafD0FAotsuufaloNeAxgPdqgFCDI4&#RljMWuPOR@S+JFjf$BE_((=DcrG~L z4-;N(twPhRY_cLp#b*tHiksBRsO{wgFt3cAn;*VA2t1pA>WA*l^DZ@mMQ+fQ``us0 z0GpJD`?z76m0^Tnpu*kYL+9Z9`D?_^huqUV)aR!2b%&^vNHC|O%hQidpCn)~^!^Qj zPNKn#s1K@R_5;=(f0Yr~DWhxwS;&&5ga>|@o}WBn?Qi>-?IZ!7SHGnzWxI3Z`NZ4= zkq5sF2cml`cTyN2JUzXnDa^APu)T#?=+JjFqZ-2sGQ8;rr^`tVWXsH#fzz_K3;~Hh9kAVt30CXNR1K@7-{}=zi z2>&Z0L5Dpd*9Z}Ny(`Is#MX749?fM#@P>GWD(3yvy z_e7RGM=@dt!e>=tufcrjUT?ZaV$<~4{ulxW5K4bzyZ(V`Dfg}Y?+@10WuO?f@s{3t z!MZ8B^mOx_B0Lsmq&zLL&#cd2xB~~0JL4t;~B$W4JH!GQ>} zTq#2Zl(Nh;oU;kygm(u@DXn2bB_;%pxrdrqWVsg9=-%su`dQ8KmKACo@s*0}R&^^; z=^x69xHx?frl3(wb8~yU+owjRD{AP*X#5ZUdpt}~N*l#&#C_WaLq(sg4^5^o8B&IexQiJmF zEK$vSbv%*15ZR~$r^GOy-sjqCdb?(6Nhvh9{;K)E2LxyJT7lZj8+UKoz4sX#!^F}2 zwc%+b08gX8Yi!YvXw&x_-*_4fQ^osU-r%?Gl7SCACkATEf;|Ia)+YWo-faMI0LL|pSk8;7jXj($)t}Z`( za{p07wQ$kWkt*cI9tmSj>pIN{YM?F>&|?ik8ee}9t*O1F&}*(A9r<2(!_)Kg64lpX zkNCCz{pjPx2>XD=p6i;PY&GzpeeSu)OTfnX(0nfHtDW&Krbc}~@lyaXP9 zZf=dsKdQ)|({rgV8kA$7Od!+FXPsIok5_sYu+*nuaI5A|l!;YlpW5V?<$96YqT0d& zoe*Yliq0w3&J6-QNJ(pRW7oO)#DJ;0eYw>z?+*NP3LJm&-9pCf-inO~s_mYhWcmT+ z5aEFBHMPkJZ7up6@X%ttg18`x+#A`7r5U-b&3HI$x8KUyxWolRK;p5aY``Rwl-s&o z9LtiAj@x@~_Rit>M+(DtwV7wR83C%D*gB`aHPNGp3OWlhG>p8MTD7|+Z0SgLV4j6L zgR-zZ7BCJmbT3iL&E5ADCF`Bf=G0x7jMLt3}u#)hnsEps8$PaYBRXVJPaVxXW?g9yQYe;b?Jp8Zc>C? z>EMldCiI8_z3U(R*IcLVWy{KcHfhaXX9O&I;T0m!`14(N$a;|fv1L3{iue4h78^ z;rE7^0kK#GrWBSMg7C_$>~|c9Ct0a@$<}hNpKGL_>7Uk&82)eC$4iB*V8jvz<)X}Q z1)6-<4MspUg7Fut)($Ck#+6#h3X|4&Lpc*}={xSPort8oU;HmEgFOKC5sbuh_x#mo zXw%()Ma)k~qscRswP-I1zJ}k&`Re@@Lat}=#yCNqk3+YkTP8xq;xB0<#9J-)y&x1j zY<-zXXihcH57wc=U!YV6=x_-=7fIj|SLapo!zJbt#i1WmneTwn>N>o)mT75UnulZ3 zga;>=Ou|WzQB7eft)M?1OR`K|{)1A28s42BT3uO&qCc_7f`X$HQ;b4EU-8o?DO;Pdkkz`xzwxd&Z+VZo`g+8W_Lt;h?#3UH*1 zH;#g*jSHDGlVror!#grG@jU1$(b^yQzIG}=OE)mrp^?xCG@z5t+WYH4w4UDWSI3ar zAzCxh>z*W;r`aDjaopqwCnx*2&04y1G;klU5)(kyGqq?`(Q7oXzT7kzzcdVL7l2QXozKIf5>ckyDl0dpqgITQ=Vq>93UN+!F z6Ed*bp_S3X34z>5?In<0=M^}!ovOaR$3s7;p}RQ&70hkPS;P$C z<|2whX%WoL{>CvdT>?6+$Mm@iqPKjGop#=uPs_Dw`OK9QVt!!u2m=uHhpH@~J@vlF zm+negN7bJ}|H@2%Kt?P%H#lSJH~uPz6hQZXjq_ZDe&eyNF@-59DOairt5s)Ay%cc4 z&g`x?EMw}I!ys<$d%~Yvg@0+PWuOc~B)^#vT5izBZqi}oV{u!rMYP*9>mO=r1EQ2Qi{xH<%T52z?>1HU9xn z0hdEKALrjS$7^k#py(L~>GCiPs@bwKqS9u4_mmr~lBA$m$f6KLg|t<;^(WS7PL{_W zAr*Cm>7eKyrA%QKQwr?&W-Ni9KFOLns-(IsA?TS(x00d(Z|78?g`-pHr;qs?VW#+C zy4Oodcy;BfRp+>3Dibfi$HheW7!S)7{#kPu^ey!i7Zy3}CZt&GEPj(4a971vLs&{y zc$nm*x8=%_;nGJc2+;F2>gI>0W--L1ZN|s)rJRX!M_moK&C-?qkg3Z~`hg;`eYg(` zYS3KD!Vbre_tbg~RYLPwN;jX)f-$%s{p*7|vasVuLr_;_863i52UqL>I@>^bzJsU~ zcBOnY$-orszmh9hR_{W>%L1;2{5?^z@gN&{?v{5J7bp|fZVEr``8$NQ zFtH(Ua+hBAJ~52D{5@5!cIspd*8R9R^B#4b)d(U7;8uYhIF z)s?Rs^cJxC^WZtf!-4josQXp8l}mxLcSqIUK#>@giLNTmbbeXXUcC`qFihP!jJ%3x zhjRF_Il4;xa%Y5Dqmvvh=XAl|i_3JD?$E9gqn}+!a!GdFkE*R|Z>T|yaFb4>y%6%W z-pl(3*7r~^;mWF=A%!iw(?Q8lcVBB26Q;+aWQ4BrAE>v3oaL~Uo$j_>UoO=J_pHWe zjR5bAq(>@^U-j(G44`btw-PETL~ECEHqg)Ntx!Vf=yb$-G+!TZGA{2<5{?d2xz?~I z4LKRAdgMP4@2aQ~>ABVOR~n~;Dh(XA$*H!~8b7nfC|EtDPG>9&#htS4Z7ir>{UP@0nR=5@&;p|aB$gtcXlg_f`^>a>z#fX8_V+Wj9Yx!>M=I z$0?6dW#PEd5cS7g=u;#Al>fu?)!8|zwNxvK#ALqwAPPpFGcWhEU-_=2U{+V^WKdGJ z&@%NsD~v{;(ZMH_%id+wY{RmsV7VrZgJkHWndpf8ncZ43{N@<~2HW;&B3VF7{b;u@ zkFrF_wbz!B7HC*dFN+G7gSZinVc<9j+Z5Z#w9Z1IO=qRajN0awVRPyhe` literal 0 HcmV?d00001 diff --git a/branding/logo/imagur.ico b/branding/logo/imagur.ico new file mode 100644 index 0000000000000000000000000000000000000000..db944e65cbf1c28449df646378cad7887bddb27b GIT binary patch literal 1150 zcmZQzU<5(|0R|wcz>vYhz#zuJz@P!dKp~(AL>x#lFaYJOffNYCfRK>Te_CNN8L|H$ zOpf{TW^(`C<~V^dh)t>)`U$%K-4{56F^Em7eiqcvX zF^Em7c{<@*aQ|zEX_BnpC|w_}-!M&o#OXIk(L?w@g>?Tbn9Kino#P0`G^bJ$R literal 0 HcmV?d00001 diff --git a/branding/logo/imagur.svg b/branding/logo/imagur.svg new file mode 100644 index 0000000..d0258aa --- /dev/null +++ b/branding/logo/imagur.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + diff --git a/frontend/package.json b/frontend/package.json index 34ca4bb..8115405 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,12 +13,14 @@ "@emotion/styled": "^11.8.1", "@mui/icons-material": "^5.4.2", "@mui/material": "^5.4.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.13.2", + "imagur-shared": "*", "notistack": "^2.0.3", "react": "^17.0.2", "react-dom": "^17.0.2", "react-dropzone": "^12.0.4", - "react-router-dom": "^6.2.1", - "imagur-shared": "*" + "react-router-dom": "^6.2.1" }, "devDependencies": { "@babel/core": "^7.17.5", diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico deleted file mode 100644 index a11777cc471a4344702741ab1c8a588998b1311a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3870 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ diff --git a/frontend/public/image/logo b/frontend/public/image/logo new file mode 120000 index 0000000..1dfef9c --- /dev/null +++ b/frontend/public/image/logo @@ -0,0 +1 @@ +/home/rubikscraft/Documents/Projects/Imagur/Imagur-Monorepo/branding/logo \ No newline at end of file diff --git a/frontend/public/index.html b/frontend/public/index.html index 068b1e1..b8d9c36 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -2,7 +2,7 @@ - + diff --git a/frontend/src/api/api.dto.ts b/frontend/src/api/api.dto.ts deleted file mode 100644 index 3aee7ba..0000000 --- a/frontend/src/api/api.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface ApiResponse { - success: boolean; - statusCode: number; - timestamp: string; - data: T; -} - -export type ApiErrorResponse = ApiResponse<{ - message: string; -}>; - -export type ApiUploadResponse = ApiResponse<{ - hash: string; -}>; diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts new file mode 100644 index 0000000..14861fb --- /dev/null +++ b/frontend/src/api/api.ts @@ -0,0 +1,133 @@ +import { AsyncFailable, Fail, HasFailed } from 'imagur-shared/dist/types'; +import { + ApiResponse, + ApiSuccessResponse, +} from 'imagur-shared/dist/dto/api.dto'; +import { validate } from 'class-validator'; +import { ClassConstructor, plainToClass } from 'class-transformer'; + +export abstract class MultiPartRequest { + public abstract createFormData(): FormData; +} + +class ImagurApiImplementation { + private async fetch( + url: RequestInfo, + options: RequestInit, + ): AsyncFailable { + try { + return await window.fetch(url, options); + } catch (e: any) { + console.warn(e); + return Fail('Something went wrong'); + } + } + + private async fetchJsonAs( + url: RequestInfo, + options: RequestInit, + ): AsyncFailable { + const response = await this.fetch(url, options); + if (HasFailed(response)) { + return response; + } + try { + return await response.json(); + } catch (e) { + console.warn(e); + return Fail('Something went wrong'); + } + } + + private async fetchBuffer( + url: RequestInfo, + options: RequestInit, + ): AsyncFailable { + const response = await this.fetch(url, options); + if (HasFailed(response)) return response; + + try { + return await response.arrayBuffer(); + } catch (e) { + console.warn(e); + return Fail('Something went wrong'); + } + } + + private async fetchSafeJson( + type: ClassConstructor, + url: RequestInfo, + options: RequestInit, + ): AsyncFailable { + let result = await this.fetchJsonAs>(url, options); + if (HasFailed(result)) return result; + + if (result.success === false) return Fail(result.data.message); + + const resultClass = plainToClass< + ApiSuccessResponse, + ApiSuccessResponse + >(ApiSuccessResponse, result); + const resultErrors = await validate(resultClass); + if (resultErrors.length > 0) { + console.warn('result', resultErrors); + return Fail('Something went wrong'); + } + + const dataClass = plainToClass(type, result.data); + const dataErrors = await validate(dataClass); + if (dataErrors.length > 0) { + console.warn('data', dataErrors); + return Fail('Something went wrong'); + } + + return result.data; + } + + public async get( + type: ClassConstructor, + url: string, + ): AsyncFailable { + return this.fetchSafeJson(type, url, { method: 'GET' }); + } + + public async post( + sendType: ClassConstructor, + receiveType: ClassConstructor, + url: string, + data: object, + ): AsyncFailable { + const sendClass = plainToClass(sendType, data); + const errors = await validate(sendClass); + if (errors.length > 0) { + console.warn(errors); + return Fail('Something went wrong'); + } + + return this.fetchSafeJson(receiveType, url, { + method: 'POST', + body: JSON.stringify(sendClass), + }); + } + + public async postForm( + receiveType: ClassConstructor, + url: string, + data: MultiPartRequest, + ): AsyncFailable { + return this.fetchSafeJson(receiveType, url, { + method: 'POST', + body: data.createFormData(), + }); + } +} + +export default abstract class ImagurApi { + private static readonly instance = new ImagurApiImplementation(); + + protected get api() { + return ImagurApi.instance; + } + + protected constructor() {} +} diff --git a/frontend/src/api/images.ts b/frontend/src/api/images.ts index f0ea07a..8196d0c 100644 --- a/frontend/src/api/images.ts +++ b/frontend/src/api/images.ts @@ -1,27 +1,47 @@ -import { AsyncFailable, Fail } from 'imagur-shared/dist/types' +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'; -export function GetImageURL(image: string): string { - const baseURL = window.location.protocol + '//' + window.location.host; - - return `${baseURL}/i/${image}`; +export interface ImageLinks { + source: string; + markdown: string; + html: string; + rst: string; + bbcode: string; } +export default class ImagesApi extends ImagurApi { + public static readonly I = new ImagesApi(); -export function ValidateImageHash(hash: string): boolean { - return /^[a-f0-9]{64}$/.test(hash); -} - -export async function UploadImage(image: File): AsyncFailable { - const formData = new FormData(); - formData.append('image', image); - - let result = await fetch('/i', { - method: 'POST', - body: formData, - }).then((res) => res.json()); - - console.log(result); - - if (!result.hash) return Fail(result.error); - - return result.hash; + protected constructor() { + super(); + } + + public async UploadImage(image: File): AsyncFailable { + const result = await this.api.postForm( + ImageUploadResponse, + '/i', + new ImageUploadRequest(image), + ); + + if (HasFailed(result)) return result; + + return result.hash; + } + + public static GetImageURL(image: string): string { + const baseURL = window.location.protocol + '//' + window.location.host; + + return `${baseURL}/i/${image}`; + } + + public static CreateImageLinks(imageURL: string) { + return { + source: imageURL, + markdown: `![image](${imageURL})`, + html: `image`, + rst: `.. image:: ${imageURL}`, + bbcode: `[img]${imageURL}[/img]`, + }; + } } diff --git a/frontend/src/api/util.ts b/frontend/src/api/util.ts deleted file mode 100644 index c408ee4..0000000 --- a/frontend/src/api/util.ts +++ /dev/null @@ -1,31 +0,0 @@ -export interface ImageLinks { - source: string; - markdown: string; - html: string; - rst: string; - bbcode: string; -} - -export function CreateImageLinks(imageURL: string) { - return { - source: imageURL, - markdown: `![image](${imageURL})`, - html: `image`, - rst: `.. image:: ${imageURL}`, - bbcode: `[img]${imageURL}[/img]`, - }; -} - -export function Debounce(fn: Function, ms: number) { - let timer: number | undefined; - - return () => { - clearTimeout(timer); - - timer = setTimeout((_) => { - timer = undefined; - - fn(); - }, ms); - }; -} diff --git a/frontend/src/frontenddto/imageroute.dto.ts b/frontend/src/frontenddto/imageroute.dto.ts new file mode 100644 index 0000000..56fc0f6 --- /dev/null +++ b/frontend/src/frontenddto/imageroute.dto.ts @@ -0,0 +1,11 @@ +import { MultiPartRequest } from '../api/api'; + +export class ImageUploadRequest implements MultiPartRequest { + constructor(private image: File) {} + + public createFormData(): FormData { + const data = new FormData(); + data.append('image', this.image); + return data; + } +} diff --git a/frontend/src/lib/debounce.ts b/frontend/src/lib/debounce.ts new file mode 100644 index 0000000..4a1c8f8 --- /dev/null +++ b/frontend/src/lib/debounce.ts @@ -0,0 +1,13 @@ +export default function Debounce(fn: Function, ms: number) { + let timer: number | undefined; + + return () => { + clearTimeout(timer); + + timer = setTimeout((_) => { + timer = undefined; + + fn(); + }, ms); + }; +} diff --git a/frontend/src/routes/processing/processing.tsx b/frontend/src/routes/processing/processing.tsx index 915124f..c4bcfbc 100644 --- a/frontend/src/routes/processing/processing.tsx +++ b/frontend/src/routes/processing/processing.tsx @@ -2,8 +2,8 @@ import Centered from '../../components/centered/centered'; import CircularProgress from '@mui/material/CircularProgress'; import { useLocation, useNavigate } from 'react-router-dom'; import { useEffect } from 'react'; -import { UploadImage } from '../../api/images'; import { HasFailed } from 'imagur-shared/dist/types'; +import ImagesApi from '../../api/images'; export interface ProcessingViewMetadata { imageFile: File; @@ -16,7 +16,7 @@ function ProcessingView(props: any) { async function onRendered() { if (!state) navigate('/'); - const hash = await UploadImage(state.imageFile); + const hash = await ImagesApi.I.UploadImage(state.imageFile); if (HasFailed(hash)) navigate('/'); // TODO: handle error navigate('/view/' + hash); diff --git a/frontend/src/routes/view/view.tsx b/frontend/src/routes/view/view.tsx index 7de71e8..2839816 100644 --- a/frontend/src/routes/view/view.tsx +++ b/frontend/src/routes/view/view.tsx @@ -1,13 +1,14 @@ import { Button, Grid, IconButton, TextField } from '@mui/material'; import { useEffect, useRef } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { GetImageURL, ValidateImageHash } from '../../api/images'; -import { CreateImageLinks, Debounce } from '../../api/util'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import Centered from '../../components/centered/centered'; import './view.css'; import { useSnackbar } from 'notistack'; +import Debounce from '../../lib/debounce'; +import { isHash } from 'class-validator'; +import ImagesApi from '../../api/images'; // Stupid names go brrr export default function ViewView() { @@ -30,7 +31,7 @@ export default function ViewView() { }; useEffect(() => { - if (!ValidateImageHash(hash)) navigate('/'); + if (!isHash(hash, 'sha256')) navigate('/'); resizeImage(); @@ -39,8 +40,8 @@ export default function ViewView() { return () => window.removeEventListener('resize', resizeImageDebounced); }); - const imageURL = GetImageURL(hash); - const imageLinks = CreateImageLinks(imageURL); + const imageURL = ImagesApi.GetImageURL(hash); + const imageLinks = ImagesApi.CreateImageLinks(imageURL); function createCopyField(label: string, value: string) { const copy = () => { diff --git a/shared/package.json b/shared/package.json index 12f3795..77a1a10 100644 --- a/shared/package.json +++ b/shared/package.json @@ -6,9 +6,10 @@ "license": "GPL-3.0", "repository": "https://github.com/rubikscraft/Imagur", "author": "Rubikscraft ", - "type": "commonjs", + "type": "module", "main": "./dist/index.js", "dependencies": { + "class-validator": "^0.13.2", "tsc-watch": "^4.6.0" }, "devDependencies": { diff --git a/shared/src/dto/api.dto.ts b/shared/src/dto/api.dto.ts new file mode 100644 index 0000000..f40dd35 --- /dev/null +++ b/shared/src/dto/api.dto.ts @@ -0,0 +1,45 @@ +import { + Equals, + IsBoolean, + IsDefined, + IsInt, + IsNotEmpty, + IsString, + Max, + Min, + ValidateNested, +} from 'class-validator'; + +class BaseApiResponse { + @IsBoolean() + @IsDefined() + success: W; + + @IsInt() + @Min(0) + @Max(1000) + @IsDefined() + statusCode: number; + + @IsString() + @IsNotEmpty() + timestamp: string; + + @ValidateNested() + @IsDefined() + data: T; +} + +export class ApiSuccessResponse extends BaseApiResponse< + T, + true +> {} + +export class ApiErrorData { + @IsString() + @IsNotEmpty() + message: string; +} +export class ApiErrorResponse extends BaseApiResponse {} + +export type ApiResponse = ApiSuccessResponse | ApiErrorResponse; diff --git a/backend/src/routes/api/auth/auth.dto.ts b/shared/src/dto/auth.dto.ts similarity index 51% rename from backend/src/routes/api/auth/auth.dto.ts rename to shared/src/dto/auth.dto.ts index f781e32..720b09a 100644 --- a/backend/src/routes/api/auth/auth.dto.ts +++ b/shared/src/dto/auth.dto.ts @@ -4,17 +4,30 @@ import { IsNotEmpty, IsOptional, IsString, + IsInt, ValidateNested, } from 'class-validator'; -import { User } from '../../../collections/userdb/user.dto'; +import { User } from './user.dto'; -export class LoginResponseDto { +// Api + +export class AuthLoginRequest { + @IsNotEmpty() + @IsString() + username: string; + + @IsNotEmpty() + @IsString() + password: string; +} + +export class AuthLoginResponse { @IsString() @IsDefined() access_token: string; } -export class RegisterRequestDto { +export class AuthRegisterRequest { @IsString() @IsNotEmpty() username: string; @@ -28,14 +41,26 @@ export class RegisterRequestDto { isAdmin?: boolean; } -export class DeleteRequestDto { +export class AuthDeleteRequest { @IsString() @IsNotEmpty() username: string; } +export class AuthDeleteResponse extends User {} + +// Extra + export class JwtDataDto { @ValidateNested() @IsDefined() user: User; + + @IsOptional() + @IsInt() + iat?: number; + + @IsOptional() + @IsInt() + exp?: number; } diff --git a/shared/src/dto/images.dto.ts b/shared/src/dto/images.dto.ts new file mode 100644 index 0000000..a2c4209 --- /dev/null +++ b/shared/src/dto/images.dto.ts @@ -0,0 +1,7 @@ +import { IsHash, IsString } from 'class-validator'; + +export class ImageUploadResponse { + @IsString() + @IsHash('sha256') + hash: string; +} diff --git a/backend/src/collections/userdb/user.dto.ts b/shared/src/dto/user.dto.ts similarity index 100% rename from backend/src/collections/userdb/user.dto.ts rename to shared/src/dto/user.dto.ts diff --git a/shared/tsconfig.json b/shared/tsconfig.json index 1377595..7826f94 100644 --- a/shared/tsconfig.json +++ b/shared/tsconfig.json @@ -9,5 +9,5 @@ "emitDecoratorMetadata": true, "sourceMap": true }, - "include": ["src"] + "include": ["src", "../backend/src/decorators/multipart.dto.ts"] }