json logging + fix lint
This commit is contained in:
parent
24f3fb9b8e
commit
13ec6e388d
18 changed files with 551 additions and 465 deletions
30
build.ts
30
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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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<VersionEntity>{
|
||||
return {
|
||||
version: process.env.npm_package_version,
|
||||
};
|
||||
}
|
||||
export class AppController {
|
||||
@Get('version')
|
||||
async getVersion(): Promise<VersionEntity> {
|
||||
return {
|
||||
version: process.env.npm_package_version,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
146
src/app.ts
146
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<NestFastifyApplication>(
|
||||
AppModule,
|
||||
new FastifyAdapter({exposeHeadRoutes: true}),
|
||||
);
|
||||
await loadServer(app);
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
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();
|
||||
|
|
|
@ -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<string, number> = {
|
||||
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<string, number> = {
|
||||
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)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export class PaginationResponse<T>{
|
||||
data: T;
|
||||
total: number;
|
||||
take: number;
|
||||
skip: number;
|
||||
export class PaginationResponse<T> {
|
||||
data: T;
|
||||
total: number;
|
||||
take: number;
|
||||
skip: number;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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') {}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export class RegisterDto{
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
export class RegisterDto {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
|
|
@ -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<UserEntity>){
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
constructor(partial: Partial<UserEntity>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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){
|
||||
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<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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<any>{
|
||||
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<any> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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<UserEntity>{
|
||||
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<UserEntity> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue