diff --git a/build.ts b/build.ts index fd486b9..0426fae 100644 --- a/build.ts +++ b/build.ts @@ -1,18 +1,18 @@ // @ts-ignore await Bun.build({ - entrypoints: ["./src/app.ts"], - format: "esm", - outdir: "./dist", - target: "node", - external: [ - "@nestjs/websockets/socket-module", - "@nestjs/microservices", - "class-transformer/storage", - "@fastify/view", - "@nestjs/platform-express", - ], - minify: { - whitespace: true, - syntax: true, - }, + entrypoints: ['./src/app.ts'], + format: 'esm', + outdir: './dist', + target: 'node', + external: [ + '@nestjs/websockets/socket-module', + '@nestjs/microservices', + 'class-transformer/storage', + '@fastify/view', + '@nestjs/platform-express', + ], + minify: { + whitespace: true, + syntax: true, + }, }); diff --git a/src/app.controller.ts b/src/app.controller.ts index 0ae9938..3b81836 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,16 +1,16 @@ -import {CacheInterceptor} from "@nestjs/cache-manager"; -import {VersionEntity} from "./common/models/entities/version.entity"; -import {Controller, Get, UseInterceptors} from "@nestjs/common"; -import {ApiTags} from "@nestjs/swagger"; +import { CacheInterceptor } from '@nestjs/cache-manager'; +import { VersionEntity } from './common/models/entities/version.entity'; +import { Controller, Get, UseInterceptors } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; @Controller() -@ApiTags("Misc") +@ApiTags('Misc') @UseInterceptors(CacheInterceptor) -export class AppController{ - @Get("version") - async getVersion(): Promise{ - return { - version: process.env.npm_package_version, - }; - } +export class AppController { + @Get('version') + async getVersion(): Promise { + return { + version: process.env.npm_package_version, + }; + } } diff --git a/src/app.ts b/src/app.ts index ce0b57d..f7c1d28 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,83 +1,91 @@ -import {FastifyAdapter, NestFastifyApplication} from "@nestjs/platform-fastify"; -import {CustomValidationPipe} from "./common/pipes/custom-validation.pipe"; -import {LoggerMiddleware} from "./common/middlewares/logger.middleware"; -import {SwaggerTheme, SwaggerThemeNameEnum} from "swagger-themes"; -import {DocumentBuilder, SwaggerModule} from "@nestjs/swagger"; -import {FastifyListenOptions} from "fastify/types/instance"; -import fastifyMultipart from "@fastify/multipart"; -import fastifyStatic from "@fastify/static"; -import fastifyHelmet from "@fastify/helmet"; -import {NestFactory} from "@nestjs/core"; -import {AppModule} from "./app.module"; -import {Logger} from "@nestjs/common"; -import * as process from "process"; -import {join} from "node:path"; +import { + FastifyAdapter, + NestFastifyApplication, +} from '@nestjs/platform-fastify'; +import { CustomValidationPipe } from './common/pipes/custom-validation.pipe'; +import { LoggerMiddleware } from './common/middlewares/logger.middleware'; +import { SwaggerTheme, SwaggerThemeNameEnum } from 'swagger-themes'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { FastifyListenOptions } from 'fastify/types/instance'; +import fastifyMultipart from '@fastify/multipart'; +import fastifyHelmet from '@fastify/helmet'; +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import { ConsoleLogger, Logger } from '@nestjs/common'; +import * as process from 'process'; -const logger: Logger = new Logger("App"); +const logger: Logger = new Logger('App'); -process.env.APP_NAME = process.env.npm_package_name.split("-").map((word: string): string => word.charAt(0).toUpperCase() + word.slice(1)).join(" "); +process.env.APP_NAME = process.env.npm_package_name + .split('-') + .map((word: string): string => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); const port: number = parseInt(process.env.PORT) || 4000; -async function bootstrap(){ - const app = await NestFactory.create( - AppModule, - new FastifyAdapter({exposeHeadRoutes: true}), - ); - await loadServer(app); +async function bootstrap() { + const app = await NestFactory.create( + AppModule, + new FastifyAdapter({ exposeHeadRoutes: true }), + { + logger: new ConsoleLogger({ + json: true, + }), + }, + ); + await loadServer(app); - await app.listen({ - port: port, - host: "0.0.0.0", - } as FastifyListenOptions); - app.enableShutdownHooks(); - logger.log(`Listening on http://0.0.0.0:${port}`); + await app.listen({ + port: port, + host: '0.0.0.0', + } as FastifyListenOptions); + app.enableShutdownHooks(); + logger.log(`Listening on http://0.0.0.0:${port}`); } -async function loadServer(server: NestFastifyApplication){ - // Config - server.setGlobalPrefix(process.env.PREFIX); - server.enableCors({ - origin: "*", - }); +async function loadServer(server: NestFastifyApplication) { + // Config + server.setGlobalPrefix(process.env.PREFIX); + server.enableCors({ + origin: '*', + }); - // Middlewares - server.use(new LoggerMiddleware().use); - await server.register(fastifyMultipart as any); - await server.register(fastifyStatic as any, { - root: join(process.cwd(), "public_answers"), - prefix: "/public_answers/", - }); - await server.register(fastifyHelmet as any, { - contentSecurityPolicy: false, - crossOriginEmbedderPolicy: false, - crossOriginOpenerPolicy: false, - crossOriginResourcePolicy: false, - } as any); + // Middlewares + server.use(new LoggerMiddleware().use); + await server.register(fastifyMultipart as any); + await server.register( + fastifyHelmet as any, + { + contentSecurityPolicy: false, + crossOriginEmbedderPolicy: false, + crossOriginOpenerPolicy: false, + crossOriginResourcePolicy: false, + } as any, + ); - // Swagger - const config = new DocumentBuilder() - .setTitle(process.env.APP_NAME) - .setDescription(`Documentation for ${process.env.APP_NAME}`) - .setVersion(process.env.npm_package_version) - .addBearerAuth() - .build(); + // Swagger + const config = new DocumentBuilder() + .setTitle(process.env.APP_NAME) + .setDescription(`Documentation for ${process.env.APP_NAME}`) + .setVersion(process.env.npm_package_version) + .addBearerAuth() + .build(); - const document = SwaggerModule.createDocument(server, config); - const theme = new SwaggerTheme(); - const customCss = theme.getBuffer(SwaggerThemeNameEnum.DARK); - SwaggerModule.setup("api", server, document, { - swaggerOptions: { - filter: true, - displayRequestDuration: true, - persistAuthorization: true, - docExpansion: "none", - tagsSorter: "alpha", - operationsSorter: "method", - }, - customCss, - }); + const document = SwaggerModule.createDocument(server, config); + const theme = new SwaggerTheme(); + const customCss = theme.getBuffer(SwaggerThemeNameEnum.DARK); + SwaggerModule.setup('api', server, document, { + swaggerOptions: { + filter: true, + displayRequestDuration: true, + persistAuthorization: true, + docExpansion: 'none', + tagsSorter: 'alpha', + operationsSorter: 'method', + }, + customCss, + }); - server.useGlobalPipes(new CustomValidationPipe()); + server.useGlobalPipes(new CustomValidationPipe()); } bootstrap(); diff --git a/src/common/middlewares/logger.middleware.ts b/src/common/middlewares/logger.middleware.ts index 5c3e531..eff7642 100644 --- a/src/common/middlewares/logger.middleware.ts +++ b/src/common/middlewares/logger.middleware.ts @@ -1,52 +1,52 @@ -import {Injectable, Logger, NestMiddleware} from "@nestjs/common"; -import {FastifyReply, FastifyRequest} from "fastify"; +import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; +import { FastifyReply, FastifyRequest } from 'fastify'; @Injectable() -export class LoggerMiddleware implements NestMiddleware{ - static logger: Logger = new Logger(LoggerMiddleware.name); +export class LoggerMiddleware implements NestMiddleware { + static logger: Logger = new Logger(LoggerMiddleware.name); - use(req: FastifyRequest["raw"], res: FastifyReply["raw"], next: () => void){ - const startTime = Date.now(); - (res as any).on("finish", () => { - const path = req.url; - try{ - const protocol = LoggerMiddleware.getProtocol(req); - const method = req.method; - if(method === "OPTIONS") - return; - const statusCode = res.statusCode; - const duration = Date.now() - startTime; - const resSize: any = res.getHeader("Content-Length") || "0"; - const intResSize = parseInt(resSize); - LoggerMiddleware.logger.log(`${protocol} ${method} ${path} ${statusCode} ${duration}ms ${intResSize}`); - LoggerMiddleware.logRequestTime(path, method, duration); - }catch(e){ - LoggerMiddleware.logger.warn(`Can't log route ${path} : ${e}`); - } - }); - next(); - } + use(req: FastifyRequest['raw'], res: FastifyReply['raw'], next: () => void) { + const startTime = Date.now(); + (res as any).on('finish', () => { + const path = req.url; + try { + const protocol = LoggerMiddleware.getProtocol(req); + const method = req.method; + if (method === 'OPTIONS') return; + const statusCode = res.statusCode; + const duration = Date.now() - startTime; + const resSize: any = res.getHeader('Content-Length') || '0'; + const intResSize = parseInt(resSize); + LoggerMiddleware.logger.log( + `${protocol} ${method} ${path} ${statusCode} ${duration}ms ${intResSize}`, + ); + LoggerMiddleware.logRequestTime(path, method, duration); + } catch (e) { + LoggerMiddleware.logger.warn(`Can't log route ${path} : ${e}`); + } + }); + next(); + } - static getProtocol(req: FastifyRequest["raw"]): string{ - const localPort: number = req.connection.localPort; - if(!localPort) - return "H2"; - return localPort.toString() === process.env.HTTPS_PORT ? "HTTPS" : "HTTP"; - } + static getProtocol(req: FastifyRequest['raw']): string { + const localPort: number = req.connection.localPort; + if (!localPort) return 'H2'; + return localPort.toString() === process.env.HTTPS_PORT ? 'HTTPS' : 'HTTP'; + } - static logRequestTime(path: string, method: string, duration: number): void{ - const thresholds: Record = { - GET: 750, - POST: 1500, - PUT: 1500, - PATCH: 500, - DELETE: 500, - }; - const threshold = thresholds[method]; - if(threshold && duration > threshold){ - LoggerMiddleware.logger.warn( - `${method} (${path}) request exceeded ${threshold}ms (${duration}ms)`, - ); - } + static logRequestTime(path: string, method: string, duration: number): void { + const thresholds: Record = { + GET: 750, + POST: 1500, + PUT: 1500, + PATCH: 500, + DELETE: 500, + }; + const threshold = thresholds[method]; + if (threshold && duration > threshold) { + LoggerMiddleware.logger.warn( + `${method} (${path}) request exceeded ${threshold}ms (${duration}ms)`, + ); } + } } diff --git a/src/common/models/dto/pagination.dto.ts b/src/common/models/dto/pagination.dto.ts index e36012f..155804a 100644 --- a/src/common/models/dto/pagination.dto.ts +++ b/src/common/models/dto/pagination.dto.ts @@ -1,13 +1,13 @@ -import {IsNumber, IsOptional, Min} from "class-validator"; +import { IsNumber, IsOptional, Min } from 'class-validator'; -export class PaginationDto{ - @IsNumber() - @Min(1) - @IsOptional() - take?: number; +export class PaginationDto { + @IsNumber() + @Min(1) + @IsOptional() + take?: number; - @IsNumber() - @Min(0) - @IsOptional() - skip?: number; + @IsNumber() + @Min(0) + @IsOptional() + skip?: number; } diff --git a/src/common/models/entities/version.entity.ts b/src/common/models/entities/version.entity.ts index 52b2a4e..eaa7c94 100644 --- a/src/common/models/entities/version.entity.ts +++ b/src/common/models/entities/version.entity.ts @@ -1,6 +1,6 @@ -import {ApiProperty} from "@nestjs/swagger"; +import { ApiProperty } from '@nestjs/swagger'; -export class VersionEntity{ - @ApiProperty() - version: string; +export class VersionEntity { + @ApiProperty() + version: string; } diff --git a/src/common/models/responses/pagination.response.ts b/src/common/models/responses/pagination.response.ts index dabd501..b9b2d63 100644 --- a/src/common/models/responses/pagination.response.ts +++ b/src/common/models/responses/pagination.response.ts @@ -1,6 +1,6 @@ -export class PaginationResponse{ - data: T; - total: number; - take: number; - skip: number; +export class PaginationResponse { + data: T; + total: number; + take: number; + skip: number; } diff --git a/src/common/modules/auth/auth.controller.ts b/src/common/modules/auth/auth.controller.ts index 9573a1b..3959fa6 100644 --- a/src/common/modules/auth/auth.controller.ts +++ b/src/common/modules/auth/auth.controller.ts @@ -1,10 +1,10 @@ -import { Controller, Delete, NotImplementedException } from "@nestjs/common"; +import { Controller, Delete, NotImplementedException } from '@nestjs/common'; -@Controller("auth") +@Controller('auth') export class AuthController { constructor() {} - @Delete("logout/all") + @Delete('logout/all') logoutAll() { throw new NotImplementedException(); } diff --git a/src/common/modules/auth/guards/jwt-auth.guard.ts b/src/common/modules/auth/guards/jwt-auth.guard.ts index 9f3a0d8..2155290 100644 --- a/src/common/modules/auth/guards/jwt-auth.guard.ts +++ b/src/common/modules/auth/guards/jwt-auth.guard.ts @@ -1,5 +1,5 @@ -import {Injectable} from "@nestjs/common"; -import {AuthGuard} from "@nestjs/passport"; +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; @Injectable() -export class JwtAuthGuard extends AuthGuard("jwt"){} +export class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/src/common/modules/auth/models/dto/local-login.dto.ts b/src/common/modules/auth/models/dto/local-login.dto.ts index 490cf74..becf8aa 100644 --- a/src/common/modules/auth/models/dto/local-login.dto.ts +++ b/src/common/modules/auth/models/dto/local-login.dto.ts @@ -1,11 +1,11 @@ -import {IsNotEmpty, IsString} from "class-validator"; +import { IsNotEmpty, IsString } from 'class-validator'; -export class LocalLoginDto{ - @IsString() - @IsNotEmpty() - email: string; +export class LocalLoginDto { + @IsString() + @IsNotEmpty() + email: string; - @IsString() - @IsNotEmpty() - password: string; + @IsString() + @IsNotEmpty() + password: string; } diff --git a/src/common/modules/auth/models/dto/register.dto.ts b/src/common/modules/auth/models/dto/register.dto.ts index ef2940f..1cc08e5 100644 --- a/src/common/modules/auth/models/dto/register.dto.ts +++ b/src/common/modules/auth/models/dto/register.dto.ts @@ -1,5 +1,5 @@ -export class RegisterDto{ - email: string; - username: string; - password: string; +export class RegisterDto { + email: string; + username: string; + password: string; } diff --git a/src/common/modules/auth/models/entities/user.entity.ts b/src/common/modules/auth/models/entities/user.entity.ts index 1172b55..4b00013 100644 --- a/src/common/modules/auth/models/entities/user.entity.ts +++ b/src/common/modules/auth/models/entities/user.entity.ts @@ -1,20 +1,20 @@ -import {Exclude} from "class-transformer"; +import { Exclude } from 'class-transformer'; -export class UserEntity{ - id: string; - email: string; - username: string; - createdAt: Date; - updatedAt: Date; - verified: boolean; +export class UserEntity { + id: string; + email: string; + username: string; + createdAt: Date; + updatedAt: Date; + verified: boolean; - @Exclude() - password: string; + @Exclude() + password: string; - @Exclude() - tokenId: string; + @Exclude() + tokenId: string; - constructor(partial: Partial){ - Object.assign(this, partial); - } + constructor(partial: Partial) { + Object.assign(this, partial); + } } diff --git a/src/common/modules/helper/cipher.service.ts b/src/common/modules/helper/cipher.service.ts index a5b2791..e5ba623 100644 --- a/src/common/modules/helper/cipher.service.ts +++ b/src/common/modules/helper/cipher.service.ts @@ -1,173 +1,248 @@ -import {Injectable} from "@nestjs/common"; -import * as crypto from "crypto"; +import { Injectable } from '@nestjs/common'; +import * as crypto from 'crypto'; @Injectable() -export class CipherService{ - // Hash functions - getSum(content: Bun.BlobOrStringOrBuffer): string{ - if(!content) content = ""; - return new Bun.CryptoHasher("sha256").update(content).digest().toString("hex"); - } +export class CipherService { + // Hash functions + getSum(content: Bun.BlobOrStringOrBuffer): string { + if (!content) content = ''; + return new Bun.CryptoHasher('sha256') + .update(content) + .digest() + .toString('hex'); + } - hashPassword(content: Bun.StringOrBuffer, cost = 10): string{ - return Bun.password.hashSync(content, { - algorithm: "argon2id", - timeCost: cost, - }); - } + hashPassword(content: Bun.StringOrBuffer, cost = 10): string { + return Bun.password.hashSync(content, { + algorithm: 'argon2id', + timeCost: cost, + }); + } - comparePassword(password: Bun.StringOrBuffer, hash: string): boolean{ - return Bun.password.verifySync(password, hash); - } + comparePassword(password: Bun.StringOrBuffer, hash: string): boolean { + return Bun.password.verifySync(password, hash); + } - // Symmetric functions - private prepareEncryptionKey(encryptionKey: string | Buffer): Buffer{ - let keyBuffer: Buffer; - if(typeof encryptionKey === "string") - keyBuffer = Buffer.from(encryptionKey); - else - keyBuffer = encryptionKey; - const key = Buffer.alloc(64); - keyBuffer.copy(key); - return key; - } + // Symmetric functions + private prepareEncryptionKey(encryptionKey: string | Buffer): Buffer { + let keyBuffer: Buffer; + if (typeof encryptionKey === 'string') + keyBuffer = Buffer.from(encryptionKey); + else keyBuffer = encryptionKey; + const key = Buffer.alloc(64); + keyBuffer.copy(key); + return key; + } - cipherSymmetric(content: string, encryptionKey: string | Buffer): string{ - if(!content) content = ""; - return this.cipherBufferSymmetric(Buffer.from(content, "utf-8"), encryptionKey).toString("hex"); - } + cipherSymmetric(content: string, encryptionKey: string | Buffer): string { + if (!content) content = ''; + return this.cipherBufferSymmetric( + Buffer.from(content, 'utf-8'), + encryptionKey, + ).toString('hex'); + } - cipherBufferSymmetric(content: Buffer, encryptionKey: string | Buffer): Buffer{ - if(!content) content = Buffer.alloc(0); - const iv = crypto.randomBytes(12); - const key = this.prepareEncryptionKey(encryptionKey); - const cipher = crypto.createCipheriv("aes-256-gcm", key.subarray(0, 32), iv); - const encrypted = Buffer.concat([cipher.update(content), cipher.final()]); - const tag = cipher.getAuthTag(); - return Buffer.concat([iv, encrypted, tag]); - } + cipherBufferSymmetric( + content: Buffer, + encryptionKey: string | Buffer, + ): Buffer { + if (!content) content = Buffer.alloc(0); + const iv = crypto.randomBytes(12); + const key = this.prepareEncryptionKey(encryptionKey); + const cipher = crypto.createCipheriv( + 'aes-256-gcm', + key.subarray(0, 32), + iv, + ); + const encrypted = Buffer.concat([cipher.update(content), cipher.final()]); + const tag = cipher.getAuthTag(); + return Buffer.concat([iv, encrypted, tag]); + } - decipherSymmetric(encryptedContent: string, encryptionKey: string | Buffer): string{ - return this.decipherBufferSymmetric(Buffer.from(encryptedContent, "hex"), encryptionKey).toString("utf-8"); - } + decipherSymmetric( + encryptedContent: string, + encryptionKey: string | Buffer, + ): string { + return this.decipherBufferSymmetric( + Buffer.from(encryptedContent, 'hex'), + encryptionKey, + ).toString('utf-8'); + } - decipherBufferSymmetric(encryptedContent: Buffer, encryptionKey: string | Buffer): Buffer{ - const iv = encryptedContent.subarray(0, 12); - const encrypted = encryptedContent.subarray(12, encryptedContent.length - 16); - const tag = encryptedContent.subarray(encryptedContent.length - 16); - const key = this.prepareEncryptionKey(encryptionKey); - const decipher = crypto.createDecipheriv("aes-256-gcm", key.subarray(0, 32), iv); - decipher.setAuthTag(tag); - try{ - return Buffer.concat([decipher.update(encrypted), decipher.final()]); - }catch(_){ - throw new Error("Decryption failed"); - } + decipherBufferSymmetric( + encryptedContent: Buffer, + encryptionKey: string | Buffer, + ): Buffer { + const iv = encryptedContent.subarray(0, 12); + const encrypted = encryptedContent.subarray( + 12, + encryptedContent.length - 16, + ); + const tag = encryptedContent.subarray(encryptedContent.length - 16); + const key = this.prepareEncryptionKey(encryptionKey); + const decipher = crypto.createDecipheriv( + 'aes-256-gcm', + key.subarray(0, 32), + iv, + ); + decipher.setAuthTag(tag); + try { + return Buffer.concat([decipher.update(encrypted), decipher.final()]); + } catch (_) { + throw new Error('Decryption failed'); } + } - cipherHardSymmetric(content: string, encryptionKey: string | Buffer, timeCost = 200000){ - if(!content) content = ""; - const salt = crypto.randomBytes(32); - const key = crypto.pbkdf2Sync(encryptionKey, salt, timeCost, 64, "sha512"); - const iv = crypto.randomBytes(16); - const cipher = crypto.createCipheriv("aes-256-cbc", key.subarray(0, 32), iv); - let encrypted = cipher.update(content, "utf-8", "hex"); - encrypted += cipher.final("hex"); - const hmac = crypto.createHmac("sha256", key.subarray(32)); - hmac.update(`${salt.toString("hex")}:${iv.toString("hex")}:${encrypted}`); - const digest = hmac.digest("hex"); - return `${salt.toString("hex")}:${iv.toString("hex")}:${encrypted}:${digest}`; + cipherHardSymmetric( + content: string, + encryptionKey: string | Buffer, + timeCost = 200000, + ) { + if (!content) content = ''; + const salt = crypto.randomBytes(32); + const key = crypto.pbkdf2Sync(encryptionKey, salt, timeCost, 64, 'sha512'); + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv( + 'aes-256-cbc', + key.subarray(0, 32), + iv, + ); + let encrypted = cipher.update(content, 'utf-8', 'hex'); + encrypted += cipher.final('hex'); + const hmac = crypto.createHmac('sha256', key.subarray(32)); + hmac.update(`${salt.toString('hex')}:${iv.toString('hex')}:${encrypted}`); + const digest = hmac.digest('hex'); + return `${salt.toString('hex')}:${iv.toString('hex')}:${encrypted}:${digest}`; + } + + decipherHardSymmetric( + encryptedContent: string, + encryptionKey: string | Buffer, + timeCost = 200000, + ) { + const [saltString, ivString, encryptedString, digest] = + encryptedContent.split(':'); + const salt = Buffer.from(saltString, 'hex'); + const key = crypto.pbkdf2Sync(encryptionKey, salt, timeCost, 64, 'sha512'); + const iv = Buffer.from(ivString, 'hex'); + const hmac = crypto.createHmac('sha256', key.subarray(32)); + hmac.update(`${saltString}:${ivString}:${encryptedString}`); + const calculatedDigest = hmac.digest('hex'); + if (calculatedDigest !== digest) throw new Error('Integrity check failed'); + const decipher = crypto.createDecipheriv( + 'aes-256-cbc', + key.subarray(0, 32), + iv, + ); + let decrypted = decipher.update(encryptedString, 'hex', 'utf-8'); + decrypted += decipher.final('utf-8'); + return decrypted; + } + + // Asymmetric functions + generateKeyPair( + modulusLength = 4096, + privateEncryptionKey = null, + ): crypto.KeyPairSyncResult { + if (!privateEncryptionKey) + console.warn( + 'No private encryption key provided, the private key will not be encrypted', + ); + let options = undefined; + if (privateEncryptionKey) { + options = { + cipher: 'aes-256-cbc', + passphrase: privateEncryptionKey, + }; } + return crypto.generateKeyPairSync('rsa', { + modulusLength: modulusLength, + publicKeyEncoding: { + type: 'spki', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + ...options, + }, + }); + } - decipherHardSymmetric(encryptedContent: string, encryptionKey: string | Buffer, timeCost = 200000){ - const [saltString, ivString, encryptedString, digest] = encryptedContent.split(":"); - const salt = Buffer.from(saltString, "hex"); - const key = crypto.pbkdf2Sync(encryptionKey, salt, timeCost, 64, "sha512"); - const iv = Buffer.from(ivString, "hex"); - const hmac = crypto.createHmac("sha256", key.subarray(32)); - hmac.update(`${saltString}:${ivString}:${encryptedString}`); - const calculatedDigest = hmac.digest("hex"); - if(calculatedDigest !== digest) - throw new Error("Integrity check failed"); - const decipher = crypto.createDecipheriv("aes-256-cbc", key.subarray(0, 32), iv); - let decrypted = decipher.update(encryptedString, "hex", "utf-8"); - decrypted += decipher.final("utf-8"); - return decrypted; - } + encryptAsymmetric(content: string, publicKey: string | Buffer): string { + if (!content) content = ''; + const buffer = Buffer.from(content, 'utf-8'); + const encrypted = crypto.publicEncrypt( + { + key: publicKey, + padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, + }, + buffer, + ); + return encrypted.toString('base64'); + } - // Asymmetric functions - generateKeyPair(modulusLength = 4096, privateEncryptionKey = null): crypto.KeyPairSyncResult{ - if(!privateEncryptionKey) - console.warn("No private encryption key provided, the private key will not be encrypted"); - let options = undefined; - if(privateEncryptionKey){ - options = { - cipher: "aes-256-cbc", - passphrase: privateEncryptionKey, - }; - } - return crypto.generateKeyPairSync("rsa", { - modulusLength: modulusLength, - publicKeyEncoding: { - type: "spki", - format: "pem", - }, - privateKeyEncoding: { - type: "pkcs8", - format: "pem", - ...options, - }, - }); - } - - encryptAsymmetric(content: string, publicKey: string | Buffer): string{ - if(!content) content = ""; - const buffer = Buffer.from(content, "utf-8"); - const encrypted = crypto.publicEncrypt({ - key: publicKey, + decryptAsymmetric( + encryptedContent: string, + privateKey: string | Buffer, + privateEncryptionKey = undefined, + ): string { + const buffer = Buffer.from(encryptedContent, 'base64'); + if (!privateEncryptionKey) + return crypto + .privateDecrypt( + { + key: privateKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, - }, buffer); - return encrypted.toString("base64"); - } + }, + buffer, + ) + .toString('utf-8'); + else + return crypto + .privateDecrypt( + { + key: privateKey, + padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, + passphrase: privateEncryptionKey, + }, + buffer, + ) + .toString('utf-8'); + } - decryptAsymmetric(encryptedContent: string, privateKey: string | Buffer, privateEncryptionKey = undefined): string{ - const buffer = Buffer.from(encryptedContent, "base64"); - if(!privateEncryptionKey) - return crypto.privateDecrypt({ - key: privateKey, - padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, - }, buffer).toString("utf-8"); - else - return crypto.privateDecrypt({ - key: privateKey, - padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, - passphrase: privateEncryptionKey, - }, buffer).toString("utf-8"); - } + // Secret functions + /** + * Generate a random string + * @param bytes Number of bytes to generate + */ + generateRandomBytes(bytes = 32): string { + return crypto.randomBytes(bytes).toString('hex'); + } - // Secret functions - /** - * Generate a random string - * @param bytes Number of bytes to generate - */ - generateRandomBytes(bytes = 32): string{ - return crypto.randomBytes(bytes).toString("hex"); - } + /** + * Generate a random number + * @param numbersNumber Number of numbers to generate + */ + generateRandomNumbers(numbersNumber = 6): string { + return Array.from({ length: numbersNumber }, () => + Math.floor(Math.random() * 10), + ).join(''); + } - /** - * Generate a random number - * @param numbersNumber Number of numbers to generate - */ - generateRandomNumbers(numbersNumber = 6): string{ - return Array.from({length: numbersNumber}, () => Math.floor(Math.random() * 10)).join(""); - } + maskSensitiveInfo( + str: string, + visibleCount: number = 4, + maskChar: string = '*', + ) { + return ( + str.slice(0, visibleCount) + + maskChar.repeat(Math.max(0, str.length - visibleCount)) + ); + } - maskSensitiveInfo(str: string, visibleCount: number = 4, maskChar: string = "*"){ - return str.slice(0, visibleCount) + maskChar.repeat(Math.max(0, str.length - visibleCount)); - } - - maskEmail(email: string){ - const [username, domain] = email.split("@"); - return `${this.maskSensitiveInfo(username)}@${domain}`; - } + maskEmail(email: string) { + const [username, domain] = email.split('@'); + return `${this.maskSensitiveInfo(username)}@${domain}`; + } } diff --git a/src/common/modules/helper/prisma.service.ts b/src/common/modules/helper/prisma.service.ts index 4305430..85b27c4 100644 --- a/src/common/modules/helper/prisma.service.ts +++ b/src/common/modules/helper/prisma.service.ts @@ -1,32 +1,34 @@ -import {Injectable, Logger, OnModuleInit} from "@nestjs/common"; -import {PrismaClient} from "@prisma/client"; +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; @Injectable() -export class PrismaService extends PrismaClient implements OnModuleInit{ - static readonly logger = new Logger(PrismaService.name); +export class PrismaService extends PrismaClient implements OnModuleInit { + static readonly logger = new Logger(PrismaService.name); - constructor(){ - super(); - return this.$extends({ - query: { - async $allOperations({operation, model, args, query}): Promise{ - const startTime: number = Date.now(); - const result: any = await query(args); - const duration: number = Date.now() - startTime; - const requestCount: number = args.length || 1; - const resultCount: number = !result ? 0 : result.length || 1; - PrismaService.logger.log(`${model.toUpperCase()} ${operation.toLowerCase()} ${duration}ms ${requestCount} ${resultCount}`); - return result; - }, - }, - }) as PrismaService; - } + constructor() { + super(); + return this.$extends({ + query: { + async $allOperations({ operation, model, args, query }): Promise { + const startTime: number = Date.now(); + const result: any = await query(args); + const duration: number = Date.now() - startTime; + const requestCount: number = args.length || 1; + const resultCount: number = !result ? 0 : result.length || 1; + PrismaService.logger.log( + `${model.toUpperCase()} ${operation.toLowerCase()} ${duration}ms ${requestCount} ${resultCount}`, + ); + return result; + }, + }, + }) as PrismaService; + } - async onModuleInit(){ - await this.$connect(); - } + async onModuleInit() { + await this.$connect(); + } - async onModuleDestroy(){ - await this.$disconnect(); - } + async onModuleDestroy() { + await this.$disconnect(); + } } diff --git a/src/common/pipes/custom-validation.pipe.ts b/src/common/pipes/custom-validation.pipe.ts index 969656a..8195d94 100644 --- a/src/common/pipes/custom-validation.pipe.ts +++ b/src/common/pipes/custom-validation.pipe.ts @@ -1,26 +1,31 @@ -import {BadRequestException, Logger, ValidationError, ValidationPipe} from "@nestjs/common"; +import { + BadRequestException, + Logger, + ValidationError, + ValidationPipe, +} from '@nestjs/common'; -export class CustomValidationPipe extends ValidationPipe{ - private readonly logger = new Logger(CustomValidationPipe.name); +export class CustomValidationPipe extends ValidationPipe { + private readonly logger = new Logger(CustomValidationPipe.name); - constructor(){ - super({ - transform: true, - transformOptions: {enableImplicitConversion: true}, - }); - } + constructor() { + super({ + transform: true, + transformOptions: { enableImplicitConversion: true }, + }); + } - createExceptionFactory(){ - return (validationErrors: ValidationError[] = []) => { - if(this.isDetailedOutputDisabled){ - return new BadRequestException(); - } - const messages = validationErrors.map(error => ({ - property: error.property, - constraints: error.constraints, - })); - // this.logger.error(messages); - return new BadRequestException(messages); - }; - } + createExceptionFactory() { + return (validationErrors: ValidationError[] = []) => { + if (this.isDetailedOutputDisabled) { + return new BadRequestException(); + } + const messages = validationErrors.map((error) => ({ + property: error.property, + constraints: error.constraints, + })); + // this.logger.error(messages); + return new BadRequestException(messages); + }; + } } diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 6b620ec..424a1e5 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -1,16 +1,16 @@ -import {Controller, Get, Req, UseGuards} from "@nestjs/common"; -import {JwtAuthGuard} from "../../common/modules/auth/guards/jwt-auth.guard"; -import {UserEntity} from "../../common/modules/auth/models/entities/user.entity"; -import {ApiBearerAuth} from "@nestjs/swagger"; +import { Controller, Get, Req, UseGuards } from '@nestjs/common'; +import { JwtAuthGuard } from '../../common/modules/auth/guards/jwt-auth.guard'; +import { UserEntity } from '../../common/modules/auth/models/entities/user.entity'; +import { ApiBearerAuth } from '@nestjs/swagger'; -@Controller("users") -export class UsersController{ - constructor(){} +@Controller('users') +export class UsersController { + constructor() {} - @Get("me") - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - getMyself(@Req() req: any): UserEntity{ - return req.user; - } + @Get('me') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + getMyself(@Req() req: any): UserEntity { + return req.user; + } } diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index f4c0921..b401e16 100644 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -1,10 +1,10 @@ -import {Module} from "@nestjs/common"; -import {UsersService} from "./users.service"; -import {UsersController} from "./users.controller"; +import { Module } from '@nestjs/common'; +import { UsersService } from './users.service'; +import { UsersController } from './users.controller'; @Module({ - providers: [UsersService], - exports: [UsersService], - controllers: [UsersController], + providers: [UsersService], + exports: [UsersService], + controllers: [UsersController], }) -export class UsersModule{} +export class UsersModule {} diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index 211b498..aa33885 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -1,56 +1,52 @@ -import {Injectable, NotFoundException} from "@nestjs/common"; -import {UserEntity} from "../../common/modules/auth/models/entities/user.entity"; -import {PrismaService} from "../../common/modules/helper/prisma.service"; +import { Injectable, NotFoundException } from '@nestjs/common'; +import { UserEntity } from '../../common/modules/auth/models/entities/user.entity'; +import { PrismaService } from '../../common/modules/helper/prisma.service'; @Injectable() -export class UsersService{ - constructor( - private readonly prismaService: PrismaService, - ){} +export class UsersService { + constructor(private readonly prismaService: PrismaService) {} - async getUserById(id: string){ - const user = await this.prismaService.users.findUnique({ - where: { - id, - }, - include: { - email_verifications: true, - }, - }); - if(!user) - throw new NotFoundException("User not found"); - return new UserEntity({ - id: user.id, - username: user.username, - email: user.email, - verified: !user.email_verifications, - password: user.password, - tokenId: user.token_id, - createdAt: user.created_at, - updatedAt: user.updated_at, - }); - } + async getUserById(id: string) { + const user = await this.prismaService.users.findUnique({ + where: { + id, + }, + include: { + email_verifications: true, + }, + }); + if (!user) throw new NotFoundException('User not found'); + return new UserEntity({ + id: user.id, + username: user.username, + email: user.email, + verified: !user.email_verifications, + password: user.password, + tokenId: user.token_id, + createdAt: user.created_at, + updatedAt: user.updated_at, + }); + } - async getUserByEmail(email: string): Promise{ - const user = await this.prismaService.users.findUnique({ - where: { - email, - }, - include: { - email_verifications: true, - }, - }); - if(!user) - throw new NotFoundException("User not found"); - return new UserEntity({ - id: user.id, - username: user.username, - email: user.email, - verified: !user.email_verifications, - password: user.password, - tokenId: user.token_id, - createdAt: user.created_at, - updatedAt: user.updated_at, - }); - } + async getUserByEmail(email: string): Promise { + const user = await this.prismaService.users.findUnique({ + where: { + email, + }, + include: { + email_verifications: true, + }, + }); + if (!user) throw new NotFoundException('User not found'); + return new UserEntity({ + id: user.id, + username: user.username, + email: user.email, + verified: !user.email_verifications, + password: user.password, + tokenId: user.token_id, + createdAt: user.created_at, + updatedAt: user.updated_at, + }); + } }