json logging + fix lint

This commit is contained in:
unurled 2025-02-07 14:54:10 +01:00
parent 24f3fb9b8e
commit 13ec6e388d
Signed by: unurled
GPG key ID: EFC5F5E709B47DDD
18 changed files with 551 additions and 465 deletions

View file

@ -1,18 +1,18 @@
// @ts-ignore // @ts-ignore
await Bun.build({ await Bun.build({
entrypoints: ["./src/app.ts"], entrypoints: ['./src/app.ts'],
format: "esm", format: 'esm',
outdir: "./dist", outdir: './dist',
target: "node", target: 'node',
external: [ external: [
"@nestjs/websockets/socket-module", '@nestjs/websockets/socket-module',
"@nestjs/microservices", '@nestjs/microservices',
"class-transformer/storage", 'class-transformer/storage',
"@fastify/view", '@fastify/view',
"@nestjs/platform-express", '@nestjs/platform-express',
], ],
minify: { minify: {
whitespace: true, whitespace: true,
syntax: true, syntax: true,
}, },
}); });

View file

@ -1,16 +1,16 @@
import {CacheInterceptor} from "@nestjs/cache-manager"; import { CacheInterceptor } from '@nestjs/cache-manager';
import {VersionEntity} from "./common/models/entities/version.entity"; import { VersionEntity } from './common/models/entities/version.entity';
import {Controller, Get, UseInterceptors} from "@nestjs/common"; import { Controller, Get, UseInterceptors } from '@nestjs/common';
import {ApiTags} from "@nestjs/swagger"; import { ApiTags } from '@nestjs/swagger';
@Controller() @Controller()
@ApiTags("Misc") @ApiTags('Misc')
@UseInterceptors(CacheInterceptor) @UseInterceptors(CacheInterceptor)
export class AppController{ export class AppController {
@Get("version") @Get('version')
async getVersion(): Promise<VersionEntity>{ async getVersion(): Promise<VersionEntity> {
return { return {
version: process.env.npm_package_version, version: process.env.npm_package_version,
}; };
} }
} }

View file

@ -1,83 +1,91 @@
import {FastifyAdapter, NestFastifyApplication} from "@nestjs/platform-fastify"; import {
import {CustomValidationPipe} from "./common/pipes/custom-validation.pipe"; FastifyAdapter,
import {LoggerMiddleware} from "./common/middlewares/logger.middleware"; NestFastifyApplication,
import {SwaggerTheme, SwaggerThemeNameEnum} from "swagger-themes"; } from '@nestjs/platform-fastify';
import {DocumentBuilder, SwaggerModule} from "@nestjs/swagger"; import { CustomValidationPipe } from './common/pipes/custom-validation.pipe';
import {FastifyListenOptions} from "fastify/types/instance"; import { LoggerMiddleware } from './common/middlewares/logger.middleware';
import fastifyMultipart from "@fastify/multipart"; import { SwaggerTheme, SwaggerThemeNameEnum } from 'swagger-themes';
import fastifyStatic from "@fastify/static"; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import fastifyHelmet from "@fastify/helmet"; import { FastifyListenOptions } from 'fastify/types/instance';
import {NestFactory} from "@nestjs/core"; import fastifyMultipart from '@fastify/multipart';
import {AppModule} from "./app.module"; import fastifyHelmet from '@fastify/helmet';
import {Logger} from "@nestjs/common"; import { NestFactory } from '@nestjs/core';
import * as process from "process"; import { AppModule } from './app.module';
import {join} from "node:path"; 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; const port: number = parseInt(process.env.PORT) || 4000;
async function bootstrap(){ async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>( const app = await NestFactory.create<NestFastifyApplication>(
AppModule, AppModule,
new FastifyAdapter({exposeHeadRoutes: true}), new FastifyAdapter({ exposeHeadRoutes: true }),
); {
await loadServer(app); logger: new ConsoleLogger({
json: true,
}),
},
);
await loadServer(app);
await app.listen({ await app.listen({
port: port, port: port,
host: "0.0.0.0", host: '0.0.0.0',
} as FastifyListenOptions); } as FastifyListenOptions);
app.enableShutdownHooks(); app.enableShutdownHooks();
logger.log(`Listening on http://0.0.0.0:${port}`); logger.log(`Listening on http://0.0.0.0:${port}`);
} }
async function loadServer(server: NestFastifyApplication){ async function loadServer(server: NestFastifyApplication) {
// Config // Config
server.setGlobalPrefix(process.env.PREFIX); server.setGlobalPrefix(process.env.PREFIX);
server.enableCors({ server.enableCors({
origin: "*", origin: '*',
}); });
// Middlewares // Middlewares
server.use(new LoggerMiddleware().use); server.use(new LoggerMiddleware().use);
await server.register(fastifyMultipart as any); await server.register(fastifyMultipart as any);
await server.register(fastifyStatic as any, { await server.register(
root: join(process.cwd(), "public_answers"), fastifyHelmet as any,
prefix: "/public_answers/", {
}); contentSecurityPolicy: false,
await server.register(fastifyHelmet as any, { crossOriginEmbedderPolicy: false,
contentSecurityPolicy: false, crossOriginOpenerPolicy: false,
crossOriginEmbedderPolicy: false, crossOriginResourcePolicy: false,
crossOriginOpenerPolicy: false, } as any,
crossOriginResourcePolicy: false, );
} as any);
// Swagger // Swagger
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle(process.env.APP_NAME) .setTitle(process.env.APP_NAME)
.setDescription(`Documentation for ${process.env.APP_NAME}`) .setDescription(`Documentation for ${process.env.APP_NAME}`)
.setVersion(process.env.npm_package_version) .setVersion(process.env.npm_package_version)
.addBearerAuth() .addBearerAuth()
.build(); .build();
const document = SwaggerModule.createDocument(server, config); const document = SwaggerModule.createDocument(server, config);
const theme = new SwaggerTheme(); const theme = new SwaggerTheme();
const customCss = theme.getBuffer(SwaggerThemeNameEnum.DARK); const customCss = theme.getBuffer(SwaggerThemeNameEnum.DARK);
SwaggerModule.setup("api", server, document, { SwaggerModule.setup('api', server, document, {
swaggerOptions: { swaggerOptions: {
filter: true, filter: true,
displayRequestDuration: true, displayRequestDuration: true,
persistAuthorization: true, persistAuthorization: true,
docExpansion: "none", docExpansion: 'none',
tagsSorter: "alpha", tagsSorter: 'alpha',
operationsSorter: "method", operationsSorter: 'method',
}, },
customCss, customCss,
}); });
server.useGlobalPipes(new CustomValidationPipe()); server.useGlobalPipes(new CustomValidationPipe());
} }
bootstrap(); bootstrap();

View file

@ -1,52 +1,52 @@
import {Injectable, Logger, NestMiddleware} from "@nestjs/common"; import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import {FastifyReply, FastifyRequest} from "fastify"; import { FastifyReply, FastifyRequest } from 'fastify';
@Injectable() @Injectable()
export class LoggerMiddleware implements NestMiddleware{ export class LoggerMiddleware implements NestMiddleware {
static logger: Logger = new Logger(LoggerMiddleware.name); static logger: Logger = new Logger(LoggerMiddleware.name);
use(req: FastifyRequest["raw"], res: FastifyReply["raw"], next: () => void){ use(req: FastifyRequest['raw'], res: FastifyReply['raw'], next: () => void) {
const startTime = Date.now(); const startTime = Date.now();
(res as any).on("finish", () => { (res as any).on('finish', () => {
const path = req.url; const path = req.url;
try{ try {
const protocol = LoggerMiddleware.getProtocol(req); const protocol = LoggerMiddleware.getProtocol(req);
const method = req.method; const method = req.method;
if(method === "OPTIONS") if (method === 'OPTIONS') return;
return; const statusCode = res.statusCode;
const statusCode = res.statusCode; const duration = Date.now() - startTime;
const duration = Date.now() - startTime; const resSize: any = res.getHeader('Content-Length') || '0';
const resSize: any = res.getHeader("Content-Length") || "0"; const intResSize = parseInt(resSize);
const intResSize = parseInt(resSize); LoggerMiddleware.logger.log(
LoggerMiddleware.logger.log(`${protocol} ${method} ${path} ${statusCode} ${duration}ms ${intResSize}`); `${protocol} ${method} ${path} ${statusCode} ${duration}ms ${intResSize}`,
LoggerMiddleware.logRequestTime(path, method, duration); );
}catch(e){ LoggerMiddleware.logRequestTime(path, method, duration);
LoggerMiddleware.logger.warn(`Can't log route ${path} : ${e}`); } catch (e) {
} LoggerMiddleware.logger.warn(`Can't log route ${path} : ${e}`);
}); }
next(); });
} next();
}
static getProtocol(req: FastifyRequest["raw"]): string{ static getProtocol(req: FastifyRequest['raw']): string {
const localPort: number = req.connection.localPort; const localPort: number = req.connection.localPort;
if(!localPort) if (!localPort) return 'H2';
return "H2"; return localPort.toString() === process.env.HTTPS_PORT ? 'HTTPS' : 'HTTP';
return localPort.toString() === process.env.HTTPS_PORT ? "HTTPS" : "HTTP"; }
}
static logRequestTime(path: string, method: string, duration: number): void{ static logRequestTime(path: string, method: string, duration: number): void {
const thresholds: Record<string, number> = { const thresholds: Record<string, number> = {
GET: 750, GET: 750,
POST: 1500, POST: 1500,
PUT: 1500, PUT: 1500,
PATCH: 500, PATCH: 500,
DELETE: 500, DELETE: 500,
}; };
const threshold = thresholds[method]; const threshold = thresholds[method];
if(threshold && duration > threshold){ if (threshold && duration > threshold) {
LoggerMiddleware.logger.warn( LoggerMiddleware.logger.warn(
`${method} (${path}) request exceeded ${threshold}ms (${duration}ms)`, `${method} (${path}) request exceeded ${threshold}ms (${duration}ms)`,
); );
}
} }
}
} }

View file

@ -1,13 +1,13 @@
import {IsNumber, IsOptional, Min} from "class-validator"; import { IsNumber, IsOptional, Min } from 'class-validator';
export class PaginationDto{ export class PaginationDto {
@IsNumber() @IsNumber()
@Min(1) @Min(1)
@IsOptional() @IsOptional()
take?: number; take?: number;
@IsNumber() @IsNumber()
@Min(0) @Min(0)
@IsOptional() @IsOptional()
skip?: number; skip?: number;
} }

View file

@ -1,6 +1,6 @@
import {ApiProperty} from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
export class VersionEntity{ export class VersionEntity {
@ApiProperty() @ApiProperty()
version: string; version: string;
} }

View file

@ -1,6 +1,6 @@
export class PaginationResponse<T>{ export class PaginationResponse<T> {
data: T; data: T;
total: number; total: number;
take: number; take: number;
skip: number; skip: number;
} }

View file

@ -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 { export class AuthController {
constructor() {} constructor() {}
@Delete("logout/all") @Delete('logout/all')
logoutAll() { logoutAll() {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View file

@ -1,5 +1,5 @@
import {Injectable} from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import {AuthGuard} from "@nestjs/passport"; import { AuthGuard } from '@nestjs/passport';
@Injectable() @Injectable()
export class JwtAuthGuard extends AuthGuard("jwt"){} export class JwtAuthGuard extends AuthGuard('jwt') {}

View file

@ -1,11 +1,11 @@
import {IsNotEmpty, IsString} from "class-validator"; import { IsNotEmpty, IsString } from 'class-validator';
export class LocalLoginDto{ export class LocalLoginDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
email: string; email: string;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
password: string; password: string;
} }

View file

@ -1,5 +1,5 @@
export class RegisterDto{ export class RegisterDto {
email: string; email: string;
username: string; username: string;
password: string; password: string;
} }

View file

@ -1,20 +1,20 @@
import {Exclude} from "class-transformer"; import { Exclude } from 'class-transformer';
export class UserEntity{ export class UserEntity {
id: string; id: string;
email: string; email: string;
username: string; username: string;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
verified: boolean; verified: boolean;
@Exclude() @Exclude()
password: string; password: string;
@Exclude() @Exclude()
tokenId: string; tokenId: string;
constructor(partial: Partial<UserEntity>){ constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial); Object.assign(this, partial);
} }
} }

View file

@ -1,173 +1,248 @@
import {Injectable} from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import * as crypto from "crypto"; import * as crypto from 'crypto';
@Injectable() @Injectable()
export class CipherService{ export class CipherService {
// Hash functions // Hash functions
getSum(content: Bun.BlobOrStringOrBuffer): string{ getSum(content: Bun.BlobOrStringOrBuffer): string {
if(!content) content = ""; if (!content) content = '';
return new Bun.CryptoHasher("sha256").update(content).digest().toString("hex"); return new Bun.CryptoHasher('sha256')
} .update(content)
.digest()
.toString('hex');
}
hashPassword(content: Bun.StringOrBuffer, cost = 10): string{ hashPassword(content: Bun.StringOrBuffer, cost = 10): string {
return Bun.password.hashSync(content, { return Bun.password.hashSync(content, {
algorithm: "argon2id", algorithm: 'argon2id',
timeCost: cost, timeCost: cost,
}); });
} }
comparePassword(password: Bun.StringOrBuffer, hash: string): boolean{ comparePassword(password: Bun.StringOrBuffer, hash: string): boolean {
return Bun.password.verifySync(password, hash); return Bun.password.verifySync(password, hash);
} }
// Symmetric functions // Symmetric functions
private prepareEncryptionKey(encryptionKey: string | Buffer): Buffer{ private prepareEncryptionKey(encryptionKey: string | Buffer): Buffer {
let keyBuffer: Buffer; let keyBuffer: Buffer;
if(typeof encryptionKey === "string") if (typeof encryptionKey === 'string')
keyBuffer = Buffer.from(encryptionKey); keyBuffer = Buffer.from(encryptionKey);
else else keyBuffer = encryptionKey;
keyBuffer = encryptionKey; const key = Buffer.alloc(64);
const key = Buffer.alloc(64); keyBuffer.copy(key);
keyBuffer.copy(key); return key;
return key; }
}
cipherSymmetric(content: string, encryptionKey: string | Buffer): string{ cipherSymmetric(content: string, encryptionKey: string | Buffer): string {
if(!content) content = ""; if (!content) content = '';
return this.cipherBufferSymmetric(Buffer.from(content, "utf-8"), encryptionKey).toString("hex"); return this.cipherBufferSymmetric(
} Buffer.from(content, 'utf-8'),
encryptionKey,
).toString('hex');
}
cipherBufferSymmetric(content: Buffer, encryptionKey: string | Buffer): Buffer{ cipherBufferSymmetric(
if(!content) content = Buffer.alloc(0); content: Buffer,
const iv = crypto.randomBytes(12); encryptionKey: string | Buffer,
const key = this.prepareEncryptionKey(encryptionKey); ): Buffer {
const cipher = crypto.createCipheriv("aes-256-gcm", key.subarray(0, 32), iv); if (!content) content = Buffer.alloc(0);
const encrypted = Buffer.concat([cipher.update(content), cipher.final()]); const iv = crypto.randomBytes(12);
const tag = cipher.getAuthTag(); const key = this.prepareEncryptionKey(encryptionKey);
return Buffer.concat([iv, encrypted, tag]); 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{ decipherSymmetric(
return this.decipherBufferSymmetric(Buffer.from(encryptedContent, "hex"), encryptionKey).toString("utf-8"); encryptedContent: string,
} encryptionKey: string | Buffer,
): string {
return this.decipherBufferSymmetric(
Buffer.from(encryptedContent, 'hex'),
encryptionKey,
).toString('utf-8');
}
decipherBufferSymmetric(encryptedContent: Buffer, encryptionKey: string | Buffer): Buffer{ decipherBufferSymmetric(
const iv = encryptedContent.subarray(0, 12); encryptedContent: Buffer,
const encrypted = encryptedContent.subarray(12, encryptedContent.length - 16); encryptionKey: string | Buffer,
const tag = encryptedContent.subarray(encryptedContent.length - 16); ): Buffer {
const key = this.prepareEncryptionKey(encryptionKey); const iv = encryptedContent.subarray(0, 12);
const decipher = crypto.createDecipheriv("aes-256-gcm", key.subarray(0, 32), iv); const encrypted = encryptedContent.subarray(
decipher.setAuthTag(tag); 12,
try{ encryptedContent.length - 16,
return Buffer.concat([decipher.update(encrypted), decipher.final()]); );
}catch(_){ const tag = encryptedContent.subarray(encryptedContent.length - 16);
throw new Error("Decryption failed"); 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){ cipherHardSymmetric(
if(!content) content = ""; content: string,
const salt = crypto.randomBytes(32); encryptionKey: string | Buffer,
const key = crypto.pbkdf2Sync(encryptionKey, salt, timeCost, 64, "sha512"); timeCost = 200000,
const iv = crypto.randomBytes(16); ) {
const cipher = crypto.createCipheriv("aes-256-cbc", key.subarray(0, 32), iv); if (!content) content = '';
let encrypted = cipher.update(content, "utf-8", "hex"); const salt = crypto.randomBytes(32);
encrypted += cipher.final("hex"); const key = crypto.pbkdf2Sync(encryptionKey, salt, timeCost, 64, 'sha512');
const hmac = crypto.createHmac("sha256", key.subarray(32)); const iv = crypto.randomBytes(16);
hmac.update(`${salt.toString("hex")}:${iv.toString("hex")}:${encrypted}`); const cipher = crypto.createCipheriv(
const digest = hmac.digest("hex"); 'aes-256-cbc',
return `${salt.toString("hex")}:${iv.toString("hex")}:${encrypted}:${digest}`; 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<string, string> {
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){ encryptAsymmetric(content: string, publicKey: string | Buffer): string {
const [saltString, ivString, encryptedString, digest] = encryptedContent.split(":"); if (!content) content = '';
const salt = Buffer.from(saltString, "hex"); const buffer = Buffer.from(content, 'utf-8');
const key = crypto.pbkdf2Sync(encryptionKey, salt, timeCost, 64, "sha512"); const encrypted = crypto.publicEncrypt(
const iv = Buffer.from(ivString, "hex"); {
const hmac = crypto.createHmac("sha256", key.subarray(32)); key: publicKey,
hmac.update(`${saltString}:${ivString}:${encryptedString}`); padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
const calculatedDigest = hmac.digest("hex"); },
if(calculatedDigest !== digest) buffer,
throw new Error("Integrity check failed"); );
const decipher = crypto.createDecipheriv("aes-256-cbc", key.subarray(0, 32), iv); return encrypted.toString('base64');
let decrypted = decipher.update(encryptedString, "hex", "utf-8"); }
decrypted += decipher.final("utf-8");
return decrypted;
}
// Asymmetric functions decryptAsymmetric(
generateKeyPair(modulusLength = 4096, privateEncryptionKey = null): crypto.KeyPairSyncResult<string, string>{ encryptedContent: string,
if(!privateEncryptionKey) privateKey: string | Buffer,
console.warn("No private encryption key provided, the private key will not be encrypted"); privateEncryptionKey = undefined,
let options = undefined; ): string {
if(privateEncryptionKey){ const buffer = Buffer.from(encryptedContent, 'base64');
options = { if (!privateEncryptionKey)
cipher: "aes-256-cbc", return crypto
passphrase: privateEncryptionKey, .privateDecrypt(
}; {
} key: privateKey,
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,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, 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{ // Secret functions
const buffer = Buffer.from(encryptedContent, "base64"); /**
if(!privateEncryptionKey) * Generate a random string
return crypto.privateDecrypt({ * @param bytes Number of bytes to generate
key: privateKey, */
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, generateRandomBytes(bytes = 32): string {
}, buffer).toString("utf-8"); return crypto.randomBytes(bytes).toString('hex');
else }
return crypto.privateDecrypt({
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
passphrase: privateEncryptionKey,
}, buffer).toString("utf-8");
}
// Secret functions /**
/** * Generate a random number
* Generate a random string * @param numbersNumber Number of numbers to generate
* @param bytes Number of bytes to generate */
*/ generateRandomNumbers(numbersNumber = 6): string {
generateRandomBytes(bytes = 32): string{ return Array.from({ length: numbersNumber }, () =>
return crypto.randomBytes(bytes).toString("hex"); Math.floor(Math.random() * 10),
} ).join('');
}
/** maskSensitiveInfo(
* Generate a random number str: string,
* @param numbersNumber Number of numbers to generate visibleCount: number = 4,
*/ maskChar: string = '*',
generateRandomNumbers(numbersNumber = 6): string{ ) {
return Array.from({length: numbersNumber}, () => Math.floor(Math.random() * 10)).join(""); return (
} str.slice(0, visibleCount) +
maskChar.repeat(Math.max(0, str.length - visibleCount))
);
}
maskSensitiveInfo(str: string, visibleCount: number = 4, maskChar: string = "*"){ maskEmail(email: string) {
return str.slice(0, visibleCount) + maskChar.repeat(Math.max(0, str.length - visibleCount)); const [username, domain] = email.split('@');
} return `${this.maskSensitiveInfo(username)}@${domain}`;
}
maskEmail(email: string){
const [username, domain] = email.split("@");
return `${this.maskSensitiveInfo(username)}@${domain}`;
}
} }

View file

@ -1,32 +1,34 @@
import {Injectable, Logger, OnModuleInit} from "@nestjs/common"; import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import {PrismaClient} from "@prisma/client"; import { PrismaClient } from '@prisma/client';
@Injectable() @Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit{ export class PrismaService extends PrismaClient implements OnModuleInit {
static readonly logger = new Logger(PrismaService.name); static readonly logger = new Logger(PrismaService.name);
constructor(){ constructor() {
super(); super();
return this.$extends({ return this.$extends({
query: { query: {
async $allOperations({operation, model, args, query}): Promise<any>{ async $allOperations({ operation, model, args, query }): Promise<any> {
const startTime: number = Date.now(); const startTime: number = Date.now();
const result: any = await query(args); const result: any = await query(args);
const duration: number = Date.now() - startTime; const duration: number = Date.now() - startTime;
const requestCount: number = args.length || 1; const requestCount: number = args.length || 1;
const resultCount: number = !result ? 0 : result.length || 1; const resultCount: number = !result ? 0 : result.length || 1;
PrismaService.logger.log(`${model.toUpperCase()} ${operation.toLowerCase()} ${duration}ms ${requestCount} ${resultCount}`); PrismaService.logger.log(
return result; `${model.toUpperCase()} ${operation.toLowerCase()} ${duration}ms ${requestCount} ${resultCount}`,
}, );
}, return result;
}) as PrismaService; },
} },
}) as PrismaService;
}
async onModuleInit(){ async onModuleInit() {
await this.$connect(); await this.$connect();
} }
async onModuleDestroy(){ async onModuleDestroy() {
await this.$disconnect(); await this.$disconnect();
} }
} }

View file

@ -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{ export class CustomValidationPipe extends ValidationPipe {
private readonly logger = new Logger(CustomValidationPipe.name); private readonly logger = new Logger(CustomValidationPipe.name);
constructor(){ constructor() {
super({ super({
transform: true, transform: true,
transformOptions: {enableImplicitConversion: true}, transformOptions: { enableImplicitConversion: true },
}); });
} }
createExceptionFactory(){ createExceptionFactory() {
return (validationErrors: ValidationError[] = []) => { return (validationErrors: ValidationError[] = []) => {
if(this.isDetailedOutputDisabled){ if (this.isDetailedOutputDisabled) {
return new BadRequestException(); return new BadRequestException();
} }
const messages = validationErrors.map(error => ({ const messages = validationErrors.map((error) => ({
property: error.property, property: error.property,
constraints: error.constraints, constraints: error.constraints,
})); }));
// this.logger.error(messages); // this.logger.error(messages);
return new BadRequestException(messages); return new BadRequestException(messages);
}; };
} }
} }

View file

@ -1,16 +1,16 @@
import {Controller, Get, Req, UseGuards} from "@nestjs/common"; import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import {JwtAuthGuard} from "../../common/modules/auth/guards/jwt-auth.guard"; import { JwtAuthGuard } from '../../common/modules/auth/guards/jwt-auth.guard';
import {UserEntity} from "../../common/modules/auth/models/entities/user.entity"; import { UserEntity } from '../../common/modules/auth/models/entities/user.entity';
import {ApiBearerAuth} from "@nestjs/swagger"; import { ApiBearerAuth } from '@nestjs/swagger';
@Controller("users") @Controller('users')
export class UsersController{ export class UsersController {
constructor(){} constructor() {}
@Get("me") @Get('me')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
getMyself(@Req() req: any): UserEntity{ getMyself(@Req() req: any): UserEntity {
return req.user; return req.user;
} }
} }

View file

@ -1,10 +1,10 @@
import {Module} from "@nestjs/common"; import { Module } from '@nestjs/common';
import {UsersService} from "./users.service"; import { UsersService } from './users.service';
import {UsersController} from "./users.controller"; import { UsersController } from './users.controller';
@Module({ @Module({
providers: [UsersService], providers: [UsersService],
exports: [UsersService], exports: [UsersService],
controllers: [UsersController], controllers: [UsersController],
}) })
export class UsersModule{} export class UsersModule {}

View file

@ -1,56 +1,52 @@
import {Injectable, NotFoundException} from "@nestjs/common"; import { Injectable, NotFoundException } from '@nestjs/common';
import {UserEntity} from "../../common/modules/auth/models/entities/user.entity"; import { UserEntity } from '../../common/modules/auth/models/entities/user.entity';
import {PrismaService} from "../../common/modules/helper/prisma.service"; import { PrismaService } from '../../common/modules/helper/prisma.service';
@Injectable() @Injectable()
export class UsersService{ export class UsersService {
constructor( constructor(private readonly prismaService: PrismaService) {}
private readonly prismaService: PrismaService,
){}
async getUserById(id: string){ async getUserById(id: string) {
const user = await this.prismaService.users.findUnique({ const user = await this.prismaService.users.findUnique({
where: { where: {
id, id,
}, },
include: { include: {
email_verifications: true, email_verifications: true,
}, },
}); });
if(!user) if (!user) throw new NotFoundException('User not found');
throw new NotFoundException("User not found"); return new UserEntity({
return new UserEntity({ id: user.id,
id: user.id, username: user.username,
username: user.username, email: user.email,
email: user.email, verified: !user.email_verifications,
verified: !user.email_verifications, password: user.password,
password: user.password, tokenId: user.token_id,
tokenId: user.token_id, createdAt: user.created_at,
createdAt: user.created_at, updatedAt: user.updated_at,
updatedAt: user.updated_at, });
}); }
}
async getUserByEmail(email: string): Promise<UserEntity>{ async getUserByEmail(email: string): Promise<UserEntity> {
const user = await this.prismaService.users.findUnique({ const user = await this.prismaService.users.findUnique({
where: { where: {
email, email,
}, },
include: { include: {
email_verifications: true, email_verifications: true,
}, },
}); });
if(!user) if (!user) throw new NotFoundException('User not found');
throw new NotFoundException("User not found"); return new UserEntity({
return new UserEntity({ id: user.id,
id: user.id, username: user.username,
username: user.username, email: user.email,
email: user.email, verified: !user.email_verifications,
verified: !user.email_verifications, password: user.password,
password: user.password, tokenId: user.token_id,
tokenId: user.token_id, createdAt: user.created_at,
createdAt: user.created_at, updatedAt: user.updated_at,
updatedAt: user.updated_at, });
}); }
}
} }