From f4c917475248df76fede0f8e01b7541dc4ead717 Mon Sep 17 00:00:00 2001 From: sauravdhakal12 Date: Sat, 21 Feb 2026 17:21:48 +0545 Subject: [PATCH] feat: Basic setup with auth --- common/exceptions/custom-exceptions.ts | 8 +++ common/exceptions/exception-filter.ts | 32 +++++++++ common/exceptions/index.ts | 1 + common/http/index.ts | 1 + common/http/response.ts | 25 +++++++ common/interceptors/index.ts | 0 common/interceptors/response.interceptor.ts | 72 +++++++++++++++++++++ package.json | 2 + pnpm-lock.yaml | 34 ++++++++++ src/app.controller.ts | 4 +- src/app.module.ts | 19 +++++- src/auth/auth.controller.ts | 55 +++++++++++++++- src/auth/auth.module.ts | 12 +++- src/auth/auth.service.ts | 71 ++++++++++++++++++-- src/auth/dto/index.ts | 2 + src/auth/dto/login-response.dto.ts | 14 ++++ src/auth/dto/login-user.dto.ts | 24 +++++++ src/auth/guards/auth.guard.ts | 2 +- src/auth/types/index.ts | 1 + src/auth/types/jwt.ts | 7 +- src/auth/types/token.ts | 11 ++++ src/main.ts | 37 +++++++++++ src/prisma/prisma.service.ts | 1 - src/user/user.module.ts | 1 + 24 files changed, 418 insertions(+), 18 deletions(-) create mode 100644 common/exceptions/custom-exceptions.ts create mode 100644 common/exceptions/exception-filter.ts create mode 100644 common/exceptions/index.ts create mode 100644 common/http/index.ts create mode 100644 common/http/response.ts create mode 100644 common/interceptors/index.ts create mode 100644 common/interceptors/response.interceptor.ts create mode 100644 src/auth/dto/login-response.dto.ts create mode 100644 src/auth/dto/login-user.dto.ts create mode 100644 src/auth/types/token.ts diff --git a/common/exceptions/custom-exceptions.ts b/common/exceptions/custom-exceptions.ts new file mode 100644 index 0000000..6448ff7 --- /dev/null +++ b/common/exceptions/custom-exceptions.ts @@ -0,0 +1,8 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; + +// Base exception +export class BaseException extends HttpException { + protected constructor(code: string, message: string, status: HttpStatus) { + super({ code, message }, status); + } +} diff --git a/common/exceptions/exception-filter.ts b/common/exceptions/exception-filter.ts new file mode 100644 index 0000000..cb29fde --- /dev/null +++ b/common/exceptions/exception-filter.ts @@ -0,0 +1,32 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + Logger, +} from '@nestjs/common'; +import { Request, Response } from 'express'; + +@Catch(HttpException) // What exception to catch +export class HttpExceptionFilter implements ExceptionFilter { + constructor(private readonly logger: Logger) {} + catch(exception: HttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const request: Request = ctx.getRequest(); + const response: Response = ctx.getResponse(); + const status = exception.getStatus(); + + if (status >= 500) { + this.logger.warn({ + method: request.method, + url: request.url, + message: exception.message, + }); + } + + response.status(status).json({ + message: exception.message, + statusCode: status, + }); + } +} diff --git a/common/exceptions/index.ts b/common/exceptions/index.ts new file mode 100644 index 0000000..cac4c69 --- /dev/null +++ b/common/exceptions/index.ts @@ -0,0 +1 @@ +export * from './custom-exceptions'; diff --git a/common/http/index.ts b/common/http/index.ts new file mode 100644 index 0000000..dbc1ea0 --- /dev/null +++ b/common/http/index.ts @@ -0,0 +1 @@ +export * from './response'; diff --git a/common/http/response.ts b/common/http/response.ts new file mode 100644 index 0000000..2a50d74 --- /dev/null +++ b/common/http/response.ts @@ -0,0 +1,25 @@ +export class MessageResponse { + readonly success: boolean; + readonly message: string; + + constructor(message?: string) { + this.success = true; + this.message = message ?? 'Success'; + } +} + +export class DataResponse extends MessageResponse { + readonly data: T; + + constructor(data: T, message?: string) { + super(message); + this.data = data; + } +} + +// Skipped +export class GlobalErrorResponseDTO { + success: boolean; + message: string; + statusCode: number; +} diff --git a/common/interceptors/index.ts b/common/interceptors/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/common/interceptors/response.interceptor.ts b/common/interceptors/response.interceptor.ts new file mode 100644 index 0000000..9927eab --- /dev/null +++ b/common/interceptors/response.interceptor.ts @@ -0,0 +1,72 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { DataResponse, MessageResponse } from 'common/http'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +@Injectable() +export class ResponseInterceptor implements NestInterceptor { + intercept(_: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + map((data) => { + if (data instanceof MessageResponse) return data; + else if (data instanceof DataResponse) return data; + else if (typeof data === 'string') return new MessageResponse(data); + return new DataResponse(data); + }), + ); + } +} + +/* NOTE: How to access request + * + @Injectable() + export class ResponseInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const req = context.switchToHttp().getRequest(); + req.userId = 'ram'; + return next.handle(); + // return next.handle().pipe( + // map((data) => { + // if (data instanceof MessageResponse) return data; + // else if (data instanceof DataResponse) return data; + // else if (typeof data === 'string') return new MessageResponse(data); + // return new DataResponse(data); + // }), + // ); + } + } + +REQUEST + ↓ +Guards + ↓ +Interceptors (before) + ↓ +Pipes + ↓ +Controller method + ↓ +Interceptors (after) ← YOU ARE HERE + ↓ +Response + + +3️⃣ What is next.handle() really? +next.handle(): Observable + + +This is: + +An RxJS stream of whatever the controller returns + +Not the request + +Not the response object + +Just the return value + * */ diff --git a/package.json b/package.json index cf4df8a..24714b9 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@nestjs/swagger": "^11.2.6", "@prisma/adapter-pg": "^7.3.0", "@prisma/client": "^7.3.0", + "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", "pg": "^8.18.0", @@ -46,6 +47,7 @@ "@nestjs/testing": "^11.0.1", "@swc/cli": "^0.6.0", "@swc/core": "^1.10.7", + "@types/bcrypt": "^6.0.0", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.10.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a80d95b..0db4edd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@prisma/client': specifier: ^7.3.0 version: 7.3.0(prisma@7.3.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3) + bcrypt: + specifier: ^6.0.0 + version: 6.0.0 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -69,6 +72,9 @@ importers: '@swc/core': specifier: ^1.10.7 version: 1.15.11 + '@types/bcrypt': + specifier: ^6.0.0 + version: 6.0.0 '@types/express': specifier: ^5.0.0 version: 5.0.6 @@ -1128,6 +1134,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/bcrypt@6.0.0': + resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} + '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -1537,6 +1546,10 @@ packages: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true + bcrypt@6.0.0: + resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} + engines: {node: '>= 18'} + bin-version-check@5.1.0: resolution: {integrity: sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==} engines: {node: '>=12'} @@ -2895,12 +2908,20 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-addon-api@8.5.0: + resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} + engines: {node: ^18 || ^20 || >= 21} + node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -4958,6 +4979,10 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/bcrypt@6.0.0': + dependencies: + '@types/node': 22.19.10 + '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 @@ -5499,6 +5524,11 @@ snapshots: baseline-browser-mapping@2.9.19: {} + bcrypt@6.0.0: + dependencies: + node-addon-api: 8.5.0 + node-gyp-build: 4.8.4 + bin-version-check@5.1.0: dependencies: bin-version: 6.0.0 @@ -7034,12 +7064,16 @@ snapshots: node-abort-controller@3.1.1: {} + node-addon-api@8.5.0: {} + node-emoji@1.11.0: dependencies: lodash: 4.17.23 node-fetch-native@1.6.7: {} + node-gyp-build@4.8.4: {} + node-int64@0.4.0: {} node-releases@2.0.27: {} diff --git a/src/app.controller.ts b/src/app.controller.ts index cce879e..90ff618 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,7 +1,9 @@ import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; +import { Public } from './auth/decorators'; -@Controller() +@Controller('') +@Public(true) export class AppController { constructor(private readonly appService: AppService) {} diff --git a/src/app.module.ts b/src/app.module.ts index d4a2753..0dff09d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,4 +1,4 @@ -import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; +import { Logger, MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { UserModule } from './user/user.module'; @@ -7,6 +7,10 @@ import { RequestContextMiddleware } from 'core/als/request-context.middleware'; import { RequestContextModule } from 'core/als/request-context.module'; import { ConfigModule } from '@nestjs/config'; import { PrismaModule } from './prisma/prisma.module'; +import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; +import { ResponseInterceptor } from 'common/interceptors/response.interceptor'; +import { ExceptionsHandler } from '@nestjs/core/exceptions/exceptions-handler'; +import { HttpExceptionFilter } from 'common/exceptions/exception-filter'; @Module({ imports: [ @@ -19,7 +23,18 @@ import { PrismaModule } from './prisma/prisma.module'; PrismaModule, ], controllers: [AppController], - providers: [AppService], + providers: [ + AppService, + Logger, + { + provide: APP_INTERCEPTOR, + useClass: ResponseInterceptor, + }, + { + provide: APP_FILTER, + useClass: HttpExceptionFilter, + }, + ], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 268eeb2..bb98ab5 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,4 +1,55 @@ -import { Controller } from '@nestjs/common'; +import { + Body, + Controller, + HttpCode, + HttpStatus, + Post, + Res, +} from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { ApiOperation } from '@nestjs/swagger'; +import { + LoginUserRequestDTO, + LoginUserResponseDTO, + RegisterUserRequestDTO, +} from './dto'; +import { Response } from 'express'; +import { DataResponse } from 'common/http'; @Controller('auth') -export class AuthController {} +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @ApiOperation({ summary: 'User login' }) + @HttpCode(HttpStatus.OK) + @Post('/login') + async login( + @Body() body: LoginUserRequestDTO, + @Res({ passthrough: true }) response: Response, + ): Promise> { + const { accessToken, refreshToken, user } = + await this.authService.login(body); + + response.cookie('accessToken', accessToken); + + return new DataResponse( + new LoginUserResponseDTO(user, accessToken, refreshToken), + 'Login successfull', + ); + } + + @ApiOperation({ summary: 'User register' }) + @HttpCode(HttpStatus.CREATED) + @Post('/register') + async register(@Body() body: RegisterUserRequestDTO): Promise { + await this.authService.register(body); + + return 'Registered successfully. Login to continue.'; + } + + logout() {} + + forgotPassword() {} + + regenTokens() {} +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 9a1b40a..d6f3dc1 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,17 +1,23 @@ -import { Module } from '@nestjs/common'; +import { Global, Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { AuthController } from './auth.controller'; -import { APP_GUARD } from '@nestjs/core'; +import { APP_GUARD, Reflector } from '@nestjs/core'; import { AuthGuard } from './guards/auth.guard'; +import { UserModule } from 'src/user/user.module'; +import { JwtModule } from '@nestjs/jwt'; +import { RequestContextModule } from 'core/als/request-context.module'; +@Global() @Module({ providers: [ AuthService, { provide: APP_GUARD, - useClass: AuthGuard, + useFactory: () => AuthGuard, + inject: [Reflector], }, ], controllers: [AuthController], + imports: [UserModule, JwtModule, RequestContextModule], }) export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 7a2bad1..88ed60a 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,13 +1,74 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { Public } from './decorators'; +import { LoginUserRequestDTO, RegisterUserRequestDTO } from './dto'; +import * as bcrypt from 'bcrypt'; +import { UserService } from 'src/user/user.service'; +import { TokenInputType } from './types'; +import { JwtService } from '@nestjs/jwt'; @Injectable() @Public() export class AuthService { - async login() {} + constructor( + private readonly userService: UserService, + private readonly jwtService: JwtService, + ) {} - async signup() {} + async register(dto: RegisterUserRequestDTO) { + const hashedPassword = await bcrypt.hash(dto.password, 10); + await this.userService.createUserWithPassword({ + ...dto, + password: hashedPassword, + }); - @Public(false) - async logout() {} + return true; + } + + async login(dto: LoginUserRequestDTO) { + const user = await this.userService.findUserForAuth(dto.email); + if (!user) throw new UnauthorizedException('Invalid credentials.'); + + const passwordMatch = await bcrypt.compare(dto.password, user.password); + if (!passwordMatch) throw new UnauthorizedException('Invalid credentials.'); + + const token = { + userId: user.id, + email: user.email, + role: user.role, + }; + + // TODO: Store more info: orgId, orgRole, etc + const { accessToken, refreshToken } = await this.genSignedTokens(token); + const hashedRefreshToken = await bcrypt.hash(refreshToken, 10); + + await this.userService.updateRefreshToken(user.id, hashedRefreshToken); + + return { + accessToken, + refreshToken, + user, + }; + } + + logout() {} + + resetPassword() {} + + // TODO: Use nest jwt + private async genSignedTokens(token: TokenInputType) { + const accessToken = await this.jwtService.signAsync(token, { + secret: 'demo', + }); + + const refreshToken = await this.jwtService.signAsync( + { + userId: token.userId, + }, + { + secret: 'demo', + }, + ); + + return { accessToken, refreshToken }; + } } diff --git a/src/auth/dto/index.ts b/src/auth/dto/index.ts index dbdf61a..8dbf399 100644 --- a/src/auth/dto/index.ts +++ b/src/auth/dto/index.ts @@ -1 +1,3 @@ export * from './register-user.dto'; +export * from './login-user.dto'; +export * from './login-response.dto'; diff --git a/src/auth/dto/login-response.dto.ts b/src/auth/dto/login-response.dto.ts new file mode 100644 index 0000000..8b978e3 --- /dev/null +++ b/src/auth/dto/login-response.dto.ts @@ -0,0 +1,14 @@ +import { User } from 'prisma/generated/prisma/client'; +import { UserDTO } from 'src/user/dtos'; + +export class LoginUserResponseDTO { + readonly accessToken: string; + readonly refreshToken: string; + readonly user: UserDTO; + + constructor(user: User, accessToken: string, refreshToken: string) { + this.user = new UserDTO(user); + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} diff --git a/src/auth/dto/login-user.dto.ts b/src/auth/dto/login-user.dto.ts new file mode 100644 index 0000000..fe8d7ff --- /dev/null +++ b/src/auth/dto/login-user.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; + +export class LoginUserRequestDTO { + @ApiProperty({ + description: "User's email", + example: 'user@example.com', + type: 'string', + }) + @IsEmail() + @IsNotEmpty() + email: string; + + @ApiProperty({ + description: "User's password", + example: '123456', + type: 'string', + minLength: 6, + }) + @IsString() + @IsNotEmpty() + @MinLength(6) + password: string; +} diff --git a/src/auth/guards/auth.guard.ts b/src/auth/guards/auth.guard.ts index 8727f0a..9a0873b 100644 --- a/src/auth/guards/auth.guard.ts +++ b/src/auth/guards/auth.guard.ts @@ -12,9 +12,9 @@ import { PUBLIC_KEY } from 'common/keys'; export class AuthGuard implements CanActivate { constructor( + private readonly reflector: Reflector, private readonly jwtService: JwtService, private readonly requestContext: RequestContextService, - private readonly reflector: Reflector, ) {} async canActivate(context: ExecutionContext) { diff --git a/src/auth/types/index.ts b/src/auth/types/index.ts index 684e2e5..6c0aafa 100644 --- a/src/auth/types/index.ts +++ b/src/auth/types/index.ts @@ -1,2 +1,3 @@ export * from './jwt'; export * from './role'; +export * from './token'; diff --git a/src/auth/types/jwt.ts b/src/auth/types/jwt.ts index 31e5137..7cd9cd4 100644 --- a/src/auth/types/jwt.ts +++ b/src/auth/types/jwt.ts @@ -1,11 +1,12 @@ -import { UserRoleType } from './role'; +import { ORG_ROLE, USER_ROLE } from 'prisma/generated/prisma/enums'; export interface JwtPayload { iat?: number; exp?: number; orgId?: string; + orgRole?: ORG_ROLE; userId: string; email: string; - role: UserRoleType; - permission?: string[]; + role: USER_ROLE; + permissions?: string[]; } diff --git a/src/auth/types/token.ts b/src/auth/types/token.ts new file mode 100644 index 0000000..37900d2 --- /dev/null +++ b/src/auth/types/token.ts @@ -0,0 +1,11 @@ +export interface TokenInputType { + userId: string; + email: string; + role: string; +} + +export interface AccessTokenPayloadType extends TokenInputType {} + +export interface RefreshTokenPayloadType { + userId: string; +} diff --git a/src/main.ts b/src/main.ts index 1a3e69b..fb4cfc2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,47 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ConfigService } from '@nestjs/config'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; async function bootstrap() { const app = await NestFactory.create(AppModule); + const swaggerConfig = new DocumentBuilder() + .setTitle('Kaa Khane') + .setDescription(`API Documentation for Kaa Khane`) + .setVersion('0.0.1') + .addGlobalResponse( + { + status: 500, + description: 'Internal Server Error', + }, + { + status: 401, + description: 'Unauthorized', + }, + { + status: 403, + description: 'Forbidden', + }, + ) + .addBearerAuth( + { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + 'access-token', + ) + .build(); + + const documentFactory = () => + SwaggerModule.createDocument(app, swaggerConfig); + SwaggerModule.setup('/docs', app, documentFactory, { + swaggerOptions: { + persistAuthorization: true, + }, + }); + const config = app.get(ConfigService); const port = config.get('PORT') ?? 3000; diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts index 4f05e74..c37b618 100644 --- a/src/prisma/prisma.service.ts +++ b/src/prisma/prisma.service.ts @@ -24,7 +24,6 @@ export class PrismaService const connectionPool = new Pool({ connectionString, }); - console.log(connectionString); const adapter = new PrismaPg(connectionPool); diff --git a/src/user/user.module.ts b/src/user/user.module.ts index 06908c2..c7da6f9 100644 --- a/src/user/user.module.ts +++ b/src/user/user.module.ts @@ -5,5 +5,6 @@ import { PrismaModule } from 'src/prisma/prisma.module'; @Module({ providers: [UserService], imports: [PrismaModule], + exports: [UserService], }) export class UserModule {}