feat: Basic setup with auth
This commit is contained in:
8
common/exceptions/custom-exceptions.ts
Normal file
8
common/exceptions/custom-exceptions.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
common/exceptions/exception-filter.ts
Normal file
32
common/exceptions/exception-filter.ts
Normal file
@@ -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<HttpException> {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
1
common/exceptions/index.ts
Normal file
1
common/exceptions/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './custom-exceptions';
|
||||||
1
common/http/index.ts
Normal file
1
common/http/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './response';
|
||||||
25
common/http/response.ts
Normal file
25
common/http/response.ts
Normal file
@@ -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<T> 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;
|
||||||
|
}
|
||||||
0
common/interceptors/index.ts
Normal file
0
common/interceptors/index.ts
Normal file
72
common/interceptors/response.interceptor.ts
Normal file
72
common/interceptors/response.interceptor.ts
Normal file
@@ -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<T> implements NestInterceptor<T, any> {
|
||||||
|
intercept(_: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
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<T> implements NestInterceptor<T, any> {
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
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<T>
|
||||||
|
|
||||||
|
|
||||||
|
This is:
|
||||||
|
|
||||||
|
An RxJS stream of whatever the controller returns
|
||||||
|
|
||||||
|
Not the request
|
||||||
|
|
||||||
|
Not the response object
|
||||||
|
|
||||||
|
Just the return value
|
||||||
|
* */
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"@nestjs/swagger": "^11.2.6",
|
"@nestjs/swagger": "^11.2.6",
|
||||||
"@prisma/adapter-pg": "^7.3.0",
|
"@prisma/adapter-pg": "^7.3.0",
|
||||||
"@prisma/client": "^7.3.0",
|
"@prisma/client": "^7.3.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
"pg": "^8.18.0",
|
"pg": "^8.18.0",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"@nestjs/testing": "^11.0.1",
|
"@nestjs/testing": "^11.0.1",
|
||||||
"@swc/cli": "^0.6.0",
|
"@swc/cli": "^0.6.0",
|
||||||
"@swc/core": "^1.10.7",
|
"@swc/core": "^1.10.7",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
|
|||||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ importers:
|
|||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^7.3.0
|
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)
|
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:
|
class-transformer:
|
||||||
specifier: ^0.5.1
|
specifier: ^0.5.1
|
||||||
version: 0.5.1
|
version: 0.5.1
|
||||||
@@ -69,6 +72,9 @@ importers:
|
|||||||
'@swc/core':
|
'@swc/core':
|
||||||
specifier: ^1.10.7
|
specifier: ^1.10.7
|
||||||
version: 1.15.11
|
version: 1.15.11
|
||||||
|
'@types/bcrypt':
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.6
|
version: 5.0.6
|
||||||
@@ -1128,6 +1134,9 @@ packages:
|
|||||||
'@types/babel__traverse@7.28.0':
|
'@types/babel__traverse@7.28.0':
|
||||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
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':
|
'@types/body-parser@1.19.6':
|
||||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||||
|
|
||||||
@@ -1537,6 +1546,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
|
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
bcrypt@6.0.0:
|
||||||
|
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
bin-version-check@5.1.0:
|
bin-version-check@5.1.0:
|
||||||
resolution: {integrity: sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==}
|
resolution: {integrity: sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -2895,12 +2908,20 @@ packages:
|
|||||||
node-abort-controller@3.1.1:
|
node-abort-controller@3.1.1:
|
||||||
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
|
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:
|
node-emoji@1.11.0:
|
||||||
resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==}
|
resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==}
|
||||||
|
|
||||||
node-fetch-native@1.6.7:
|
node-fetch-native@1.6.7:
|
||||||
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
|
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:
|
node-int64@0.4.0:
|
||||||
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
||||||
|
|
||||||
@@ -4958,6 +4979,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
|
|
||||||
|
'@types/bcrypt@6.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 22.19.10
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
@@ -5499,6 +5524,11 @@ snapshots:
|
|||||||
|
|
||||||
baseline-browser-mapping@2.9.19: {}
|
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:
|
bin-version-check@5.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
bin-version: 6.0.0
|
bin-version: 6.0.0
|
||||||
@@ -7034,12 +7064,16 @@ snapshots:
|
|||||||
|
|
||||||
node-abort-controller@3.1.1: {}
|
node-abort-controller@3.1.1: {}
|
||||||
|
|
||||||
|
node-addon-api@8.5.0: {}
|
||||||
|
|
||||||
node-emoji@1.11.0:
|
node-emoji@1.11.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash: 4.17.23
|
lodash: 4.17.23
|
||||||
|
|
||||||
node-fetch-native@1.6.7: {}
|
node-fetch-native@1.6.7: {}
|
||||||
|
|
||||||
|
node-gyp-build@4.8.4: {}
|
||||||
|
|
||||||
node-int64@0.4.0: {}
|
node-int64@0.4.0: {}
|
||||||
|
|
||||||
node-releases@2.0.27: {}
|
node-releases@2.0.27: {}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { Public } from './auth/decorators';
|
||||||
|
|
||||||
@Controller()
|
@Controller('')
|
||||||
|
@Public(true)
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly appService: AppService) {}
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { UserModule } from './user/user.module';
|
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 { RequestContextModule } from 'core/als/request-context.module';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { PrismaModule } from './prisma/prisma.module';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -19,7 +23,18 @@ import { PrismaModule } from './prisma/prisma.module';
|
|||||||
PrismaModule,
|
PrismaModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [
|
||||||
|
AppService,
|
||||||
|
Logger,
|
||||||
|
{
|
||||||
|
provide: APP_INTERCEPTOR,
|
||||||
|
useClass: ResponseInterceptor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: APP_FILTER,
|
||||||
|
useClass: HttpExceptionFilter,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppModule implements NestModule {
|
export class AppModule implements NestModule {
|
||||||
configure(consumer: MiddlewareConsumer) {
|
configure(consumer: MiddlewareConsumer) {
|
||||||
|
|||||||
@@ -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')
|
@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<DataResponse<LoginUserResponseDTO>> {
|
||||||
|
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<string> {
|
||||||
|
await this.authService.register(body);
|
||||||
|
|
||||||
|
return 'Registered successfully. Login to continue.';
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {}
|
||||||
|
|
||||||
|
forgotPassword() {}
|
||||||
|
|
||||||
|
regenTokens() {}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthController } from './auth.controller';
|
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 { 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({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
AuthService,
|
AuthService,
|
||||||
{
|
{
|
||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: AuthGuard,
|
useFactory: () => AuthGuard,
|
||||||
|
inject: [Reflector],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
|
imports: [UserModule, JwtModule, RequestContextModule],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@@ -1,13 +1,74 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { Public } from './decorators';
|
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()
|
@Injectable()
|
||||||
@Public()
|
@Public()
|
||||||
export class AuthService {
|
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)
|
return true;
|
||||||
async logout() {}
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
export * from './register-user.dto';
|
export * from './register-user.dto';
|
||||||
|
export * from './login-user.dto';
|
||||||
|
export * from './login-response.dto';
|
||||||
|
|||||||
14
src/auth/dto/login-response.dto.ts
Normal file
14
src/auth/dto/login-response.dto.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/auth/dto/login-user.dto.ts
Normal file
24
src/auth/dto/login-user.dto.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -12,9 +12,9 @@ import { PUBLIC_KEY } from 'common/keys';
|
|||||||
|
|
||||||
export class AuthGuard implements CanActivate {
|
export class AuthGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly reflector: Reflector,
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly requestContext: RequestContextService,
|
private readonly requestContext: RequestContextService,
|
||||||
private readonly reflector: Reflector,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext) {
|
async canActivate(context: ExecutionContext) {
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './jwt';
|
export * from './jwt';
|
||||||
export * from './role';
|
export * from './role';
|
||||||
|
export * from './token';
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { UserRoleType } from './role';
|
import { ORG_ROLE, USER_ROLE } from 'prisma/generated/prisma/enums';
|
||||||
|
|
||||||
export interface JwtPayload {
|
export interface JwtPayload {
|
||||||
iat?: number;
|
iat?: number;
|
||||||
exp?: number;
|
exp?: number;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
|
orgRole?: ORG_ROLE;
|
||||||
userId: string;
|
userId: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: UserRoleType;
|
role: USER_ROLE;
|
||||||
permission?: string[];
|
permissions?: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/auth/types/token.ts
Normal file
11
src/auth/types/token.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface TokenInputType {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessTokenPayloadType extends TokenInputType {}
|
||||||
|
|
||||||
|
export interface RefreshTokenPayloadType {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
37
src/main.ts
37
src/main.ts
@@ -1,10 +1,47 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
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 config = app.get(ConfigService);
|
||||||
const port = config.get<number>('PORT') ?? 3000;
|
const port = config.get<number>('PORT') ?? 3000;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export class PrismaService
|
|||||||
const connectionPool = new Pool({
|
const connectionPool = new Pool({
|
||||||
connectionString,
|
connectionString,
|
||||||
});
|
});
|
||||||
console.log(connectionString);
|
|
||||||
|
|
||||||
const adapter = new PrismaPg(connectionPool);
|
const adapter = new PrismaPg(connectionPool);
|
||||||
|
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ import { PrismaModule } from 'src/prisma/prisma.module';
|
|||||||
@Module({
|
@Module({
|
||||||
providers: [UserService],
|
providers: [UserService],
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
|
exports: [UserService],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user