init commit
This commit is contained in:
commit
2346fe4879
45 changed files with 2573 additions and 0 deletions
9
.dockerignore
Normal file
9
.dockerignore
Normal file
|
@ -0,0 +1,9 @@
|
|||
node_modules/
|
||||
.git/
|
||||
dist/
|
||||
|
||||
.env*
|
||||
.flaskenv*
|
||||
!.env.project
|
||||
!.env.vault
|
||||
public_answers/
|
4
.env.example
Normal file
4
.env.example
Normal file
|
@ -0,0 +1,4 @@
|
|||
DATABASE_URL=
|
||||
SECRET_KEY=
|
||||
# Redis
|
||||
REDIS_URL=
|
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
/build
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# temp directory
|
||||
.temp
|
||||
.tmp
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
4
.prettierrc
Normal file
4
.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
22
Dockerfile
Normal file
22
Dockerfile
Normal file
|
@ -0,0 +1,22 @@
|
|||
FROM oven/bun:alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY bun.lock ./
|
||||
COPY package.json ./
|
||||
COPY tsconfig.json ./
|
||||
|
||||
RUN bun install --production --frozen-lockfile
|
||||
|
||||
COPY prisma ./prisma/
|
||||
RUN bunx prisma generate
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN bun run build
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
CMD bunx prisma migrate deploy && bunx prisma db seed && bun run start:prod
|
18
build.ts
Normal file
18
build.ts
Normal file
|
@ -0,0 +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,
|
||||
},
|
||||
});
|
2
bunfig.toml
Normal file
2
bunfig.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[run]
|
||||
bun = true
|
20
compose.dev.yaml
Normal file
20
compose.dev.yaml
Normal file
|
@ -0,0 +1,20 @@
|
|||
services:
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: bracket
|
||||
ports:
|
||||
- '5432:5432'
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data
|
||||
redis:
|
||||
image: redis
|
||||
restart: always
|
||||
ports:
|
||||
- '6379:6379'
|
||||
|
||||
volumes:
|
||||
database:
|
35
eslint.config.mjs
Normal file
35
eslint.config.mjs
Normal file
|
@ -0,0 +1,35 @@
|
|||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['eslint.config.mjs'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
ecmaVersion: 5,
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn'
|
||||
},
|
||||
},
|
||||
);
|
16
nest-cli.json
Normal file
16
nest-cli.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"entryFile": "app",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true,
|
||||
"plugins": [{
|
||||
"name": "@nestjs/swagger",
|
||||
"options": {
|
||||
"dtoFileNameSuffix": [".dto.ts", ".entity.ts", ".response.ts", ".payload.ts"],
|
||||
"introspectComments": true
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
60
package.json
Normal file
60
package.json
Normal file
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"name": "nestjs-template",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "bun build.ts",
|
||||
"start": "bun --bun nest start",
|
||||
"start:dev": "bun --hot ./src/app.ts",
|
||||
"start:prod": "bun dist/app",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/helmet": "^13.0.1",
|
||||
"@fastify/multipart": "^9.0.3",
|
||||
"@fastify/static": "^8.1.0",
|
||||
"@keyv/redis": "^4.2.0",
|
||||
"@nestjs/cache-manager": "^3.0.0",
|
||||
"@nestjs/common": "^11.0.8",
|
||||
"@nestjs/config": "^4.0.0",
|
||||
"@nestjs/core": "^11.0.8",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-fastify": "^11.0.8",
|
||||
"@nestjs/schedule": "^5.0.1",
|
||||
"@nestjs/swagger": "^11.0.3",
|
||||
"@nestjs/throttler": "^6.4.0",
|
||||
"@prisma/client": "6.3.1",
|
||||
"cache-manager": "^6.4.0",
|
||||
"cacheable": "^1.8.8",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"fastify": "^5.2.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nodemailer": "^6.10.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"prisma": "^6.3.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"swagger-themes": "^1.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^11.0.2",
|
||||
"@stylistic/eslint-plugin": "^3.0.1",
|
||||
"@types/bun": "^1.2.2",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@typescript-eslint/parser": "^8.23.0",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.23.0"
|
||||
}
|
||||
}
|
15
prisma/migrations/20250207113919_init/migration.sql
Normal file
15
prisma/migrations/20250207113919_init/migration.sql
Normal file
|
@ -0,0 +1,15 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" VARCHAR(36) NOT NULL,
|
||||
"email" VARCHAR(320) NOT NULL,
|
||||
"username" VARCHAR(30) NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
"token_id" VARCHAR(64) NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
20
prisma/schema.prisma
Normal file
20
prisma/schema.prisma
Normal file
|
@ -0,0 +1,20 @@
|
|||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Users {
|
||||
id String @id @db.VarChar(36)
|
||||
email String @unique @db.VarChar(320)
|
||||
username String @db.VarChar(30)
|
||||
password String
|
||||
token_id String @db.VarChar(64) // Authenticate JWT for security
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
@@map("users")
|
||||
}
|
16
src/app.controller.ts
Normal file
16
src/app.controller.ts
Normal file
|
@ -0,0 +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";
|
||||
|
||||
@Controller()
|
||||
@ApiTags("Misc")
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
export class AppController{
|
||||
@Get("version")
|
||||
async getVersion(): Promise<VersionEntity>{
|
||||
return {
|
||||
version: process.env.npm_package_version,
|
||||
};
|
||||
}
|
||||
}
|
44
src/app.module.ts
Normal file
44
src/app.module.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { HelperModule } from './common/modules/helper/helper.module';
|
||||
import { ClassSerializerInterceptor, Module } from '@nestjs/common';
|
||||
import { AuthModule } from './common/modules/auth/auth.module';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { AppController } from './app.controller';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { CacheableMemory } from 'cacheable';
|
||||
import KeyvRedis from '@keyv/redis';
|
||||
|
||||
@Module({
|
||||
controllers: [AppController],
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
ScheduleModule.forRoot(),
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 60000,
|
||||
limit: 60,
|
||||
},
|
||||
]),
|
||||
ScheduleModule.forRoot(),
|
||||
CacheModule.registerAsync({
|
||||
isGlobal: true,
|
||||
useFactory: async (): Promise<any> => {
|
||||
const redisUrl: string | undefined = process.env.REDIS_URL;
|
||||
return {
|
||||
stores: [redisUrl ? new KeyvRedis(redisUrl) : new CacheableMemory()],
|
||||
};
|
||||
},
|
||||
}),
|
||||
HelperModule,
|
||||
AuthModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: ClassSerializerInterceptor,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
83
src/app.ts
Normal file
83
src/app.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
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";
|
||||
|
||||
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(" ");
|
||||
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);
|
||||
|
||||
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: "*",
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
server.useGlobalPipes(new CustomValidationPipe());
|
||||
}
|
||||
|
||||
bootstrap();
|
52
src/common/middlewares/logger.middleware.ts
Normal file
52
src/common/middlewares/logger.middleware.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
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);
|
||||
|
||||
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 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)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
13
src/common/models/dto/pagination.dto.ts
Normal file
13
src/common/models/dto/pagination.dto.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import {IsNumber, IsOptional, Min} from "class-validator";
|
||||
|
||||
export class PaginationDto{
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@IsOptional()
|
||||
take?: number;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
skip?: number;
|
||||
}
|
6
src/common/models/entities/version.entity.ts
Normal file
6
src/common/models/entities/version.entity.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import {ApiProperty} from "@nestjs/swagger";
|
||||
|
||||
export class VersionEntity{
|
||||
@ApiProperty()
|
||||
version: string;
|
||||
}
|
6
src/common/models/responses/pagination.response.ts
Normal file
6
src/common/models/responses/pagination.response.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export class PaginationResponse<T>{
|
||||
data: T;
|
||||
total: number;
|
||||
take: number;
|
||||
skip: number;
|
||||
}
|
11
src/common/modules/auth/auth.controller.ts
Normal file
11
src/common/modules/auth/auth.controller.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Controller, Delete, NotImplementedException } from "@nestjs/common";
|
||||
|
||||
@Controller("auth")
|
||||
export class AuthController {
|
||||
constructor() {}
|
||||
|
||||
@Delete("logout/all")
|
||||
logoutAll() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
35
src/common/modules/auth/auth.module.ts
Normal file
35
src/common/modules/auth/auth.module.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { RegisterController } from './register.controller';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { LoginController } from './login.controller';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { LoginService } from './login.service';
|
||||
import { RegisterService } from './register.service';
|
||||
import { UsersModule } from '../../../modules/users/users.module';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuthJwtStrategy } from './strategies/auth-jwt.strategy';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController, LoginController, RegisterController],
|
||||
providers: [JwtStrategy, AuthJwtStrategy, LoginService, RegisterService],
|
||||
imports: [
|
||||
JwtModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
secret: configService.get<string>('SECRET_KEY'),
|
||||
signOptions: {
|
||||
expiresIn: '7d',
|
||||
algorithm: 'HS512',
|
||||
issuer: configService.get<string>('APP_NAME'),
|
||||
},
|
||||
verifyOptions: {
|
||||
algorithms: ['HS512'],
|
||||
issuer: configService.get<string>('APP_NAME'),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
UsersModule,
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
5
src/common/modules/auth/guards/jwt-auth.guard.ts
Normal file
5
src/common/modules/auth/guards/jwt-auth.guard.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import {Injectable} from "@nestjs/common";
|
||||
import {AuthGuard} from "@nestjs/passport";
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard("jwt"){}
|
94
src/common/modules/auth/login.controller.ts
Normal file
94
src/common/modules/auth/login.controller.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
NotImplementedException,
|
||||
Post,
|
||||
Req,
|
||||
UnauthorizedException,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { LoginPayload } from './models/payloads/login.payload';
|
||||
import { LocalLoginDto } from './models/dto/local-login.dto';
|
||||
import { UserEntity } from './models/entities/user.entity';
|
||||
import { JwtScope } from './models/enums/jwt-scope';
|
||||
import { LoginService } from './login.service';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { UsersService } from '../../../modules/users/users.service';
|
||||
|
||||
@Controller('auth/login')
|
||||
@ApiTags('Auth')
|
||||
export class LoginController {
|
||||
constructor(
|
||||
private readonly loginService: LoginService,
|
||||
private readonly usersService: UsersService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Logs in the user using email and password
|
||||
*
|
||||
* @throws {401} Invalid password
|
||||
* @throws {404} User not found
|
||||
* @throws {500} Internal server error
|
||||
*/
|
||||
@Post('')
|
||||
async login(@Body() body: LocalLoginDto): Promise<LoginPayload> {
|
||||
const user: UserEntity = await this.loginService.validateUser(
|
||||
body.email,
|
||||
body.password,
|
||||
);
|
||||
if (!(await this.loginService.isUserVerified(user.id)))
|
||||
throw new UnauthorizedException('User not verified');
|
||||
const token: string = this.loginService.generateToken(
|
||||
user.id,
|
||||
user.tokenId,
|
||||
JwtScope.USAGE,
|
||||
);
|
||||
return new LoginPayload({
|
||||
user,
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the login callback for the authentication process (Magic Link, Passkey, 2FA)
|
||||
*
|
||||
* @throws {401} Unauthorized
|
||||
* @throws {500} Internal server error
|
||||
*/
|
||||
@Post('callback')
|
||||
@UseGuards(AuthGuard('auth-jwt'))
|
||||
@ApiBearerAuth()
|
||||
loginCallback(@Req() req: any): Promise<LoginPayload> {
|
||||
// TODO: Fix this line
|
||||
req = req.user;
|
||||
if (req.scope === JwtScope.MAGIC) {
|
||||
// Check for 2FA/Passkey
|
||||
const token: string = this.loginService.generateToken(
|
||||
req.user.id,
|
||||
req.user.tokenId,
|
||||
JwtScope.USAGE,
|
||||
);
|
||||
return new LoginPayload({
|
||||
user: req.user,
|
||||
token,
|
||||
});
|
||||
}
|
||||
const authToken: string = this.loginService.generateToken(
|
||||
req.user.id,
|
||||
req.user.tokenId,
|
||||
JwtScope.USAGE,
|
||||
);
|
||||
return new LoginPayload({
|
||||
user: req.user,
|
||||
token: authToken,
|
||||
});
|
||||
}
|
||||
|
||||
@Post('passkey')
|
||||
loginPasskey() {
|
||||
// Generate JWT token
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
46
src/common/modules/auth/login.service.ts
Normal file
46
src/common/modules/auth/login.service.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { UserEntity } from './models/entities/user.entity';
|
||||
import { CipherService } from '../helper/cipher.service';
|
||||
import { PrismaService } from '../helper/prisma.service';
|
||||
import { JwtScope } from './models/enums/jwt-scope';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { UsersService } from '../../../modules/users/users.service';
|
||||
|
||||
@Injectable()
|
||||
export class LoginService {
|
||||
constructor(
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly cipherService: CipherService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly usersService: UsersService,
|
||||
) {}
|
||||
|
||||
async validateUser(email: string, password: string): Promise<UserEntity> {
|
||||
const user: UserEntity = await this.usersService.getUserByEmail(email);
|
||||
if (!this.cipherService.comparePassword(password, user.password))
|
||||
throw new UnauthorizedException('Invalid password');
|
||||
return user;
|
||||
}
|
||||
|
||||
async isUserVerified(userId: string): Promise<boolean> {
|
||||
const user: UserEntity = await this.usersService.getUserById(userId);
|
||||
return user.verified;
|
||||
}
|
||||
|
||||
generateToken(userId: string, userTokenId: string, scope: JwtScope): string {
|
||||
return this.jwtService.sign(
|
||||
{
|
||||
scope,
|
||||
},
|
||||
{
|
||||
subject: userId,
|
||||
expiresIn: scope !== JwtScope.USAGE ? '5m' : '7d',
|
||||
jwtid: userTokenId,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
11
src/common/modules/auth/models/dto/local-login.dto.ts
Normal file
11
src/common/modules/auth/models/dto/local-login.dto.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import {IsNotEmpty, IsString} from "class-validator";
|
||||
|
||||
export class LocalLoginDto{
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
}
|
5
src/common/modules/auth/models/dto/register.dto.ts
Normal file
5
src/common/modules/auth/models/dto/register.dto.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export class RegisterDto{
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
20
src/common/modules/auth/models/entities/user.entity.ts
Normal file
20
src/common/modules/auth/models/entities/user.entity.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import {Exclude} from "class-transformer";
|
||||
|
||||
export class UserEntity{
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
verified: boolean;
|
||||
|
||||
@Exclude()
|
||||
password: string;
|
||||
|
||||
@Exclude()
|
||||
tokenId: string;
|
||||
|
||||
constructor(partial: Partial<UserEntity>){
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
4
src/common/modules/auth/models/enums/jwt-scope.ts
Normal file
4
src/common/modules/auth/models/enums/jwt-scope.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export enum JwtScope {
|
||||
AUTH = 'AUTH',
|
||||
USAGE = 'USAGE',
|
||||
}
|
9
src/common/modules/auth/models/payloads/login.payload.ts
Normal file
9
src/common/modules/auth/models/payloads/login.payload.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { UserEntity } from '../entities/user.entity';
|
||||
export class LoginPayload {
|
||||
user: UserEntity;
|
||||
token: string;
|
||||
|
||||
constructor(partial: Partial<LoginPayload>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
27
src/common/modules/auth/register.controller.ts
Normal file
27
src/common/modules/auth/register.controller.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Body, Controller, HttpCode, Post } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { RegisterDto } from './models/dto/register.dto';
|
||||
import { RegisterService } from './register.service';
|
||||
|
||||
@Controller('auth/register')
|
||||
@ApiTags('Auth')
|
||||
export class RegisterController {
|
||||
constructor(private readonly registerService: RegisterService) {}
|
||||
|
||||
/**
|
||||
* Registers a new user
|
||||
*
|
||||
* @throws {400} Bad request
|
||||
* @throws {409} Email already used
|
||||
* @throws {500} Internal server error
|
||||
*/
|
||||
@Post('')
|
||||
@HttpCode(204)
|
||||
async register(@Body() body: RegisterDto): Promise<void> {
|
||||
await this.registerService.register(
|
||||
body.email,
|
||||
body.username,
|
||||
body.password,
|
||||
);
|
||||
}
|
||||
}
|
45
src/common/modules/auth/register.service.ts
Normal file
45
src/common/modules/auth/register.service.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../helper/prisma.service';
|
||||
import { CipherService } from '../helper/cipher.service';
|
||||
import {
|
||||
EmailVerifications,
|
||||
Passkeys,
|
||||
TwoFactorAuth,
|
||||
Users,
|
||||
} from '@prisma/client';
|
||||
import { UserEntity } from './models/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class RegisterService {
|
||||
constructor(
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly cipherService: CipherService,
|
||||
) {}
|
||||
|
||||
async register(
|
||||
email: string,
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<void> {
|
||||
const emailExists: Users = await this.prismaService.users.findFirst({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
if (emailExists) throw new ConflictException('Email already registered');
|
||||
const hashedPassword: string = this.cipherService.hashPassword(password);
|
||||
await this.prismaService.users.create({
|
||||
data: {
|
||||
id: Bun.randomUUIDv7(),
|
||||
email,
|
||||
username,
|
||||
password: hashedPassword,
|
||||
token_id: this.cipherService.generateRandomBytes(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
32
src/common/modules/auth/strategies/auth-jwt.strategy.ts
Normal file
32
src/common/modules/auth/strategies/auth-jwt.strategy.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { UsersService } from '../../../../modules/users/users.service';
|
||||
import { JwtPayload } from 'jsonwebtoken';
|
||||
import { JwtScope } from '../models/enums/jwt-scope';
|
||||
import { UserEntity } from '../models/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class AuthJwtStrategy extends PassportStrategy(Strategy, 'auth-jwt') {
|
||||
constructor(private readonly usersService: UsersService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: process.env.SECRET_KEY,
|
||||
issuer: process.env.APP_NAME,
|
||||
algorithms: ['HS512'],
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload): Promise<any> {
|
||||
if (payload.scope === JwtScope.USAGE)
|
||||
throw new UnauthorizedException('Invalid scope');
|
||||
const user: UserEntity = await this.usersService.getUserById(payload.sub);
|
||||
if (user.tokenId !== payload.jti)
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
return {
|
||||
user,
|
||||
scope: payload.scope,
|
||||
};
|
||||
}
|
||||
}
|
29
src/common/modules/auth/strategies/jwt.strategy.ts
Normal file
29
src/common/modules/auth/strategies/jwt.strategy.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { UserEntity } from '../models/entities/user.entity';
|
||||
import { JwtScope } from '../models/enums/jwt-scope';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { UsersService } from '../../../../modules/users/users.service';
|
||||
import { JwtPayload } from 'jsonwebtoken';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private readonly usersService: UsersService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: process.env.SECRET_KEY,
|
||||
issuer: process.env.APP_NAME,
|
||||
algorithms: ['HS512'],
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload) {
|
||||
if (payload.scope !== JwtScope.USAGE)
|
||||
throw new UnauthorizedException('Invalid scope');
|
||||
const user: UserEntity = await this.usersService.getUserById(payload.sub);
|
||||
if (user.tokenId !== payload.jti)
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
return user;
|
||||
}
|
||||
}
|
173
src/common/modules/helper/cipher.service.ts
Normal file
173
src/common/modules/helper/cipher.service.ts
Normal file
|
@ -0,0 +1,173 @@
|
|||
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");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
|
||||
maskEmail(email: string){
|
||||
const [username, domain] = email.split("@");
|
||||
return `${this.maskSensitiveInfo(username)}@${domain}`;
|
||||
}
|
||||
}
|
10
src/common/modules/helper/helper.module.ts
Normal file
10
src/common/modules/helper/helper.module.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { CipherService } from './cipher.service';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [CipherService, PrismaService],
|
||||
exports: [CipherService, PrismaService],
|
||||
})
|
||||
export class HelperModule {}
|
32
src/common/modules/helper/prisma.service.ts
Normal file
32
src/common/modules/helper/prisma.service.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
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);
|
||||
|
||||
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 onModuleDestroy(){
|
||||
await this.$disconnect();
|
||||
}
|
||||
}
|
26
src/common/pipes/custom-validation.pipe.ts
Normal file
26
src/common/pipes/custom-validation.pipe.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import {BadRequestException, Logger, ValidationError, ValidationPipe} from "@nestjs/common";
|
||||
|
||||
export class CustomValidationPipe extends ValidationPipe{
|
||||
private readonly logger = new Logger(CustomValidationPipe.name);
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
}
|
16
src/modules/users/users.controller.ts
Normal file
16
src/modules/users/users.controller.ts
Normal file
|
@ -0,0 +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";
|
||||
|
||||
@Controller("users")
|
||||
export class UsersController{
|
||||
constructor(){}
|
||||
|
||||
@Get("me")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
getMyself(@Req() req: any): UserEntity{
|
||||
return req.user;
|
||||
}
|
||||
}
|
10
src/modules/users/users.module.ts
Normal file
10
src/modules/users/users.module.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import {Module} from "@nestjs/common";
|
||||
import {UsersService} from "./users.service";
|
||||
import {UsersController} from "./users.controller";
|
||||
|
||||
@Module({
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
controllers: [UsersController],
|
||||
})
|
||||
export class UsersModule{}
|
56
src/modules/users/users.service.ts
Normal file
56
src/modules/users/users.service.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
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,
|
||||
){}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue