@anmol0493/fullstack-app 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +24 -0
- package/backend-express/.env.example +2 -0
- package/backend-express/index.js +2 -0
- package/backend-express/package-lock.json +1939 -0
- package/backend-express/package.json +25 -0
- package/backend-express/src/config/db.js +20 -0
- package/backend-express/src/controller/auth.js +71 -0
- package/backend-express/src/middleware/auth.js +36 -0
- package/backend-express/src/middleware/validator.js +16 -0
- package/backend-express/src/models/user.js +26 -0
- package/backend-express/src/routes/auth.js +25 -0
- package/backend-express/src/server.js +28 -0
- package/backend-express/src/utils/constants.js +14 -0
- package/backend-express/src/utils/helper.js +30 -0
- package/backend-express/src/utils/schema/auth.js +34 -0
- package/backend-nestjs/.env.example +3 -0
- package/backend-nestjs/.prettierrc +4 -0
- package/backend-nestjs/README.md +99 -0
- package/backend-nestjs/eslint.config.mjs +35 -0
- package/backend-nestjs/nest-cli.json +8 -0
- package/backend-nestjs/package.json +99 -0
- package/backend-nestjs/pnpm-lock.yaml +7848 -0
- package/backend-nestjs/src/app.controller.ts +12 -0
- package/backend-nestjs/src/app.module.ts +13 -0
- package/backend-nestjs/src/app.service.ts +8 -0
- package/backend-nestjs/src/common/decorators/user.decorator.ts +8 -0
- package/backend-nestjs/src/common/dtos/common.dto.ts +87 -0
- package/backend-nestjs/src/common/enum/index.ts +0 -0
- package/backend-nestjs/src/common/exceptions/custom.exception.ts +28 -0
- package/backend-nestjs/src/common/filters/http-exception.filter.ts +46 -0
- package/backend-nestjs/src/common/guard/permission.guard.ts +18 -0
- package/backend-nestjs/src/common/middleware/auth.middleware.ts +20 -0
- package/backend-nestjs/src/common/pipes/validation.pipe.ts +61 -0
- package/backend-nestjs/src/common/utils/constants.ts +36 -0
- package/backend-nestjs/src/common/utils/helper.ts +43 -0
- package/backend-nestjs/src/core/core.module.ts +8 -0
- package/backend-nestjs/src/core/jwt/jwt.module.ts +10 -0
- package/backend-nestjs/src/core/jwt/jwt.service.ts +45 -0
- package/backend-nestjs/src/core/prisma/prisma.module.ts +9 -0
- package/backend-nestjs/src/core/prisma/prisma.service.ts +17 -0
- package/backend-nestjs/src/core/prisma/schema.prisma +27 -0
- package/backend-nestjs/src/main.ts +26 -0
- package/backend-nestjs/src/module/auth/auth.controller.ts +30 -0
- package/backend-nestjs/src/module/auth/auth.module.ts +10 -0
- package/backend-nestjs/src/module/auth/auth.service.ts +83 -0
- package/backend-nestjs/src/module/auth/dto/index.ts +28 -0
- package/backend-nestjs/src/module/index.module.ts +17 -0
- package/backend-nestjs/src/scripts/migrate.js +40 -0
- package/backend-nestjs/tsconfig.build.json +4 -0
- package/backend-nestjs/tsconfig.json +22 -0
- package/frontend/.env.example +1 -0
- package/frontend/README.md +54 -0
- package/frontend/eslint.config.js +28 -0
- package/frontend/index.html +13 -0
- package/frontend/package-lock.json +3813 -0
- package/frontend/package.json +43 -0
- package/frontend/public/vite.svg +1 -0
- package/frontend/src/App.tsx +24 -0
- package/frontend/src/assets/react.svg +1 -0
- package/frontend/src/components/Layout.tsx +59 -0
- package/frontend/src/components/ui/AlertDialog.tsx +47 -0
- package/frontend/src/components/ui/Button.tsx +61 -0
- package/frontend/src/components/ui/CommonAlertDialog.tsx +57 -0
- package/frontend/src/components/ui/FormInput.tsx +73 -0
- package/frontend/src/components/ui/Loader.tsx +7 -0
- package/frontend/src/hook/useFetchUser.ts +38 -0
- package/frontend/src/index.css +1 -0
- package/frontend/src/lib/constants.ts +24 -0
- package/frontend/src/lib/schema.ts +12 -0
- package/frontend/src/lib/utils.ts +71 -0
- package/frontend/src/main.tsx +11 -0
- package/frontend/src/pages/Home.tsx +5 -0
- package/frontend/src/pages/Login.tsx +67 -0
- package/frontend/src/pages/Signup.tsx +67 -0
- package/frontend/src/redux/api/auth.ts +19 -0
- package/frontend/src/redux/slice/auth.ts +39 -0
- package/frontend/src/redux/store.ts +30 -0
- package/frontend/src/routes/index.tsx +20 -0
- package/frontend/src/routes/middleware.ts +18 -0
- package/frontend/src/types/index.ts +12 -0
- package/frontend/src/vite-env.d.ts +1 -0
- package/frontend/tsconfig.app.json +26 -0
- package/frontend/tsconfig.json +7 -0
- package/frontend/tsconfig.node.json +24 -0
- package/frontend/vite.config.ts +19 -0
- package/package.json +34 -0
- package/scripts/setup.js +73 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Controller, Get } from '@nestjs/common';
|
|
2
|
+
import { AppService } from './app.service';
|
|
3
|
+
|
|
4
|
+
@Controller()
|
|
5
|
+
export class AppController {
|
|
6
|
+
constructor(private readonly appService: AppService) {}
|
|
7
|
+
|
|
8
|
+
@Get()
|
|
9
|
+
getHello(): string {
|
|
10
|
+
return this.appService.getHello();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { AppController } from './app.controller';
|
|
3
|
+
import { AppService } from './app.service';
|
|
4
|
+
import { ConfigModule } from '@nestjs/config';
|
|
5
|
+
import { CoreModule } from './core/core.module';
|
|
6
|
+
import { IndexModule } from './module/index.module';
|
|
7
|
+
|
|
8
|
+
@Module({
|
|
9
|
+
imports: [ConfigModule.forRoot({ isGlobal: true }), CoreModule, IndexModule],
|
|
10
|
+
controllers: [AppController],
|
|
11
|
+
providers: [AppService]
|
|
12
|
+
})
|
|
13
|
+
export class AppModule {}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Transform, Type } from 'class-transformer';
|
|
2
|
+
import {
|
|
3
|
+
IsBoolean,
|
|
4
|
+
IsInt,
|
|
5
|
+
IsOptional,
|
|
6
|
+
IsString,
|
|
7
|
+
ValidateNested
|
|
8
|
+
} from 'class-validator';
|
|
9
|
+
|
|
10
|
+
export class ResponseDto {
|
|
11
|
+
@IsInt()
|
|
12
|
+
status_code: number;
|
|
13
|
+
|
|
14
|
+
@IsBoolean()
|
|
15
|
+
error: boolean;
|
|
16
|
+
|
|
17
|
+
@IsString()
|
|
18
|
+
message: string;
|
|
19
|
+
|
|
20
|
+
constructor(response: {
|
|
21
|
+
status_code: number;
|
|
22
|
+
error: boolean;
|
|
23
|
+
message: string;
|
|
24
|
+
}) {
|
|
25
|
+
this.status_code = response.status_code;
|
|
26
|
+
this.error = response.error;
|
|
27
|
+
this.message = response.message;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ResponseBaseDto<T> extends ResponseDto {
|
|
32
|
+
@Type(() => Object)
|
|
33
|
+
result: T;
|
|
34
|
+
|
|
35
|
+
constructor(response: {
|
|
36
|
+
status_code: number;
|
|
37
|
+
error: boolean;
|
|
38
|
+
message: string;
|
|
39
|
+
result: T;
|
|
40
|
+
}) {
|
|
41
|
+
super({
|
|
42
|
+
status_code: response.status_code,
|
|
43
|
+
error: response.error,
|
|
44
|
+
message: response.message
|
|
45
|
+
});
|
|
46
|
+
this.result = response.result;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class ResponseBaseWithCountDto<T> extends ResponseDto {
|
|
51
|
+
@ValidateNested({ each: true })
|
|
52
|
+
@Type(() => Object)
|
|
53
|
+
result: {
|
|
54
|
+
data: T[];
|
|
55
|
+
count: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
constructor(response: {
|
|
59
|
+
status_code: number;
|
|
60
|
+
error: boolean;
|
|
61
|
+
message: string;
|
|
62
|
+
result: {
|
|
63
|
+
data: T[];
|
|
64
|
+
count: number;
|
|
65
|
+
};
|
|
66
|
+
}) {
|
|
67
|
+
super({
|
|
68
|
+
status_code: response.status_code,
|
|
69
|
+
error: response.error,
|
|
70
|
+
message: response.message
|
|
71
|
+
});
|
|
72
|
+
this.result = response.result;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class RequestQueryDto {
|
|
77
|
+
@IsOptional()
|
|
78
|
+
@Transform(({ value }) => (value ? Number(value) : undefined))
|
|
79
|
+
page?: number;
|
|
80
|
+
|
|
81
|
+
@IsOptional()
|
|
82
|
+
@Transform(({ value }) => (value ? Number(value) : undefined))
|
|
83
|
+
limit?: number;
|
|
84
|
+
|
|
85
|
+
@IsOptional()
|
|
86
|
+
search?: string;
|
|
87
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { HttpException, HttpStatus } from '@nestjs/common';
|
|
2
|
+
import { ERROR_MSG } from 'src/common/utils/constants';
|
|
3
|
+
|
|
4
|
+
export class ResourceNotFound extends HttpException {
|
|
5
|
+
constructor(resourceName: string, resourceId?: string) {
|
|
6
|
+
super(
|
|
7
|
+
resourceId
|
|
8
|
+
? `${resourceName} with ${resourceId} not found`
|
|
9
|
+
: `${resourceName} not found`,
|
|
10
|
+
HttpStatus.NOT_FOUND
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class Unauthorized extends HttpException {
|
|
16
|
+
constructor() {
|
|
17
|
+
super(ERROR_MSG.UNAUTHORIZED, HttpStatus.UNAUTHORIZED);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class CustomException extends HttpException {
|
|
22
|
+
constructor(
|
|
23
|
+
message: string = ERROR_MSG.INTERNAL_SERVER_ERROR,
|
|
24
|
+
status_code: HttpStatus = HttpStatus.BAD_REQUEST
|
|
25
|
+
) {
|
|
26
|
+
super(message, status_code);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExceptionFilter,
|
|
3
|
+
Catch,
|
|
4
|
+
ArgumentsHost,
|
|
5
|
+
HttpException,
|
|
6
|
+
HttpStatus
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import { Response } from 'express';
|
|
9
|
+
import { ERROR_MSG } from 'src/common/utils/constants';
|
|
10
|
+
|
|
11
|
+
@Catch()
|
|
12
|
+
export class HttpExceptionFilter implements ExceptionFilter {
|
|
13
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
14
|
+
const ctx = host.switchToHttp();
|
|
15
|
+
const response = ctx.getResponse<Response>();
|
|
16
|
+
const request = ctx.getRequest<Request>();
|
|
17
|
+
|
|
18
|
+
const status =
|
|
19
|
+
exception instanceof HttpException
|
|
20
|
+
? exception.getStatus()
|
|
21
|
+
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
22
|
+
const message =
|
|
23
|
+
exception instanceof HttpException
|
|
24
|
+
? exception.getResponse()
|
|
25
|
+
: ERROR_MSG.INTERNAL_SERVER_ERROR;
|
|
26
|
+
|
|
27
|
+
const errorResponse: any = {
|
|
28
|
+
status_code: status,
|
|
29
|
+
error: true,
|
|
30
|
+
message,
|
|
31
|
+
timestamp: new Date().toISOString(),
|
|
32
|
+
path: request.url,
|
|
33
|
+
method: request.method
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (process.env.NODE_ENV !== 'prod') {
|
|
37
|
+
console.error('🚨 Error Details:', exception);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return response.status(status).json({
|
|
41
|
+
status_code: status,
|
|
42
|
+
error: true,
|
|
43
|
+
message
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
|
2
|
+
import { CustomException } from '../exceptions/custom.exception';
|
|
3
|
+
import { ERROR_MSG } from '../utils/constants';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class CheckPermissionGuard implements CanActivate {
|
|
7
|
+
constructor(private readonly screen: string) {}
|
|
8
|
+
|
|
9
|
+
canActivate(context: ExecutionContext): boolean {
|
|
10
|
+
const request = context.switchToHttp().getRequest();
|
|
11
|
+
const user = request.user;
|
|
12
|
+
|
|
13
|
+
if (!user.permissions.some((p) => p.screen === this.screen && p.value))
|
|
14
|
+
throw new CustomException(ERROR_MSG.UNAUTHORIZED);
|
|
15
|
+
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
2
|
+
import { NextFunction } from 'express';
|
|
3
|
+
import { Unauthorized } from '../exceptions/custom.exception';
|
|
4
|
+
import { AuthService } from 'src/module/auth/auth.service';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class AuthMiddleware implements NestMiddleware {
|
|
8
|
+
constructor(private readonly authService: AuthService) {}
|
|
9
|
+
async use(req: Request, res: Response, next: NextFunction) {
|
|
10
|
+
const authHeader = req.headers['authorization'];
|
|
11
|
+
if (!authHeader) throw new Unauthorized();
|
|
12
|
+
|
|
13
|
+
const token = authHeader.split(' ')[1];
|
|
14
|
+
const user = await this.authService.verifyToken(token);
|
|
15
|
+
if (!user) throw new Unauthorized();
|
|
16
|
+
|
|
17
|
+
req['user'] = user;
|
|
18
|
+
next();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
|
|
2
|
+
import { validate, ValidationError } from 'class-validator';
|
|
3
|
+
import { plainToInstance } from 'class-transformer';
|
|
4
|
+
import { CustomException } from '../exceptions/custom.exception';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class ValidationPipe implements PipeTransform<any> {
|
|
8
|
+
async transform(value: any, { metatype }: ArgumentMetadata) {
|
|
9
|
+
const trimmedValue = this.trimStrings(value);
|
|
10
|
+
if (!metatype || !this.toValidate(metatype)) return trimmedValue;
|
|
11
|
+
|
|
12
|
+
const object = plainToInstance(metatype, trimmedValue);
|
|
13
|
+
const errors = await validate(object, {
|
|
14
|
+
whitelist: true,
|
|
15
|
+
forbidNonWhitelisted: true
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (errors.length > 0) {
|
|
19
|
+
const errorMessage = this.formatErrors(errors);
|
|
20
|
+
throw new CustomException(errorMessage);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return object;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private trimStrings(value: any): any {
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
29
|
+
} else if (Array.isArray(value)) {
|
|
30
|
+
return value.map((item) => this.trimStrings(item));
|
|
31
|
+
} else if (value && typeof value === 'object' && value !== null) {
|
|
32
|
+
const trimmedObject: Record<string, any> = {};
|
|
33
|
+
for (const key in value) {
|
|
34
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
35
|
+
if (key === 'email')
|
|
36
|
+
trimmedObject[key] = this.trimStrings(value[key]).toLowerCase();
|
|
37
|
+
else trimmedObject[key] = this.trimStrings(value[key]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return trimmedObject;
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private toValidate(metatype: Function): boolean {
|
|
46
|
+
const types: Function[] = [String, Boolean, Number, Array, Object];
|
|
47
|
+
return !types.includes(metatype);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private formatErrors(errors: ValidationError[]): string {
|
|
51
|
+
return errors
|
|
52
|
+
.map((error) => {
|
|
53
|
+
if (error.constraints) {
|
|
54
|
+
return Object.values(error.constraints).join(', ');
|
|
55
|
+
} else if (error.children) {
|
|
56
|
+
return this.formatErrors(error.children);
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.join(', ');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const REQ_ENV = ['JWT_SECRET', 'DATABASE_URL'];
|
|
2
|
+
|
|
3
|
+
const MESSAGES = {
|
|
4
|
+
SUCCESS_MSG: {
|
|
5
|
+
REGISTERED: 'Registered successfully',
|
|
6
|
+
LOG_IN: 'Log in successfully',
|
|
7
|
+
LOGGED_OUT: 'Logged out successfully',
|
|
8
|
+
|
|
9
|
+
CREATED: 'Created successfully',
|
|
10
|
+
UPDATED: 'Updated successfully',
|
|
11
|
+
DELETED: 'Deleted successfully',
|
|
12
|
+
FETCHED: 'Fetched successfully'
|
|
13
|
+
},
|
|
14
|
+
ERROR_MSG: {
|
|
15
|
+
JSON_WEB_TOKEN_ERROR: 'JsonWebTokenError',
|
|
16
|
+
TOKEN_EXPIRED_ERROR: 'TokenExpiredError',
|
|
17
|
+
TOKEN_EXPIRED: 'Token has expired!',
|
|
18
|
+
|
|
19
|
+
EMAIL_EXISTS: 'Email already exists with another user',
|
|
20
|
+
INVALID_PASSWORD: 'Invalid password',
|
|
21
|
+
UNAUTHORIZED: 'Unauthorized',
|
|
22
|
+
FORBIDDEN: 'Forbidden',
|
|
23
|
+
INTERNAL_SERVER_ERROR: 'Internal server error',
|
|
24
|
+
INVALID_TOKEN: 'Token Expired'
|
|
25
|
+
}
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export const { SUCCESS_MSG, ERROR_MSG } = MESSAGES;
|
|
29
|
+
|
|
30
|
+
export const RESOURCE_NAME = {
|
|
31
|
+
USER: 'User',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const DEFAULT_LIMIT = {
|
|
35
|
+
|
|
36
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { HttpStatus } from '@nestjs/common';
|
|
2
|
+
import bcrypt from 'bcrypt';
|
|
3
|
+
|
|
4
|
+
export const encryptPassword = async (password: string): Promise<string> =>
|
|
5
|
+
await bcrypt.hash(password, 10);
|
|
6
|
+
|
|
7
|
+
export const comparePassword = async (
|
|
8
|
+
password: string,
|
|
9
|
+
hash: string
|
|
10
|
+
): Promise<boolean> => await bcrypt.compare(password, hash);
|
|
11
|
+
|
|
12
|
+
export function checkEnvVariables(requiredEnvVars: string[]) {
|
|
13
|
+
requiredEnvVars.forEach((envVar) => {
|
|
14
|
+
if (!process.env[envVar]) {
|
|
15
|
+
console.error(
|
|
16
|
+
`\x1b[31mMissing required environment variable: ${envVar}\x1b[0m`
|
|
17
|
+
);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const successResponse = <T>(message: string) =>
|
|
24
|
+
({
|
|
25
|
+
status_code: HttpStatus.OK,
|
|
26
|
+
error: false,
|
|
27
|
+
message
|
|
28
|
+
}) as const;
|
|
29
|
+
|
|
30
|
+
export const successResponseWithResult = <T>(message: string, result: T) =>
|
|
31
|
+
({
|
|
32
|
+
status_code: HttpStatus.OK,
|
|
33
|
+
error: false,
|
|
34
|
+
message,
|
|
35
|
+
result
|
|
36
|
+
}) as const;
|
|
37
|
+
|
|
38
|
+
export function formatResponse<T>(data: T[], count: number) {
|
|
39
|
+
return {
|
|
40
|
+
data,
|
|
41
|
+
count
|
|
42
|
+
} as const;
|
|
43
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Global, Module } from '@nestjs/common';
|
|
2
|
+
import { JwtService as JWTService } from '@nestjs/jwt';
|
|
3
|
+
import { JwtService } from './jwt.service';
|
|
4
|
+
|
|
5
|
+
@Global()
|
|
6
|
+
@Module({
|
|
7
|
+
providers: [JWTService, JwtService],
|
|
8
|
+
exports: [JwtService]
|
|
9
|
+
})
|
|
10
|
+
export class JwtModule {}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { HttpStatus, Injectable } from '@nestjs/common';
|
|
2
|
+
import { JwtService as JWTService } from '@nestjs/jwt';
|
|
3
|
+
import { CustomException } from 'src/common/exceptions/custom.exception';
|
|
4
|
+
import { ERROR_MSG } from 'src/common/utils/constants';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class JwtService {
|
|
8
|
+
private readonly secret: string;
|
|
9
|
+
private readonly expiresIn: string;
|
|
10
|
+
|
|
11
|
+
constructor(private readonly jwtService: JWTService) {
|
|
12
|
+
const { JWT_SECRET, JWT_EXP_IN } = process.env;
|
|
13
|
+
this.secret = JWT_SECRET as string;
|
|
14
|
+
this.expiresIn = JWT_EXP_IN as string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async signAsync(payload: any, expiresIn?: string): Promise<string> {
|
|
18
|
+
return this.jwtService.signAsync(payload, {
|
|
19
|
+
secret: this.secret,
|
|
20
|
+
expiresIn: expiresIn || this.expiresIn
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async verifyAsync(token: string): Promise<any> {
|
|
25
|
+
try {
|
|
26
|
+
const decoded = await this.jwtService.verifyAsync(token, {
|
|
27
|
+
secret: this.secret
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return decoded;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.name === ERROR_MSG.TOKEN_EXPIRED_ERROR) {
|
|
33
|
+
throw new CustomException(
|
|
34
|
+
ERROR_MSG.TOKEN_EXPIRED,
|
|
35
|
+
HttpStatus.UNAUTHORIZED
|
|
36
|
+
);
|
|
37
|
+
} else {
|
|
38
|
+
throw new CustomException(
|
|
39
|
+
ERROR_MSG.UNAUTHORIZED,
|
|
40
|
+
HttpStatus.UNAUTHORIZED
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { PrismaClient } from '@prisma/client';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class PrismaService
|
|
6
|
+
extends PrismaClient
|
|
7
|
+
implements OnModuleInit, OnModuleDestroy
|
|
8
|
+
{
|
|
9
|
+
async onModuleInit() {
|
|
10
|
+
await this.$connect();
|
|
11
|
+
console.log('Connected to Prisma Client');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async onModuleDestroy() {
|
|
15
|
+
await this.$disconnect();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// This is your Prisma schema file,
|
|
2
|
+
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
3
|
+
|
|
4
|
+
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
|
5
|
+
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
|
6
|
+
|
|
7
|
+
generator client {
|
|
8
|
+
provider = "prisma-client-js"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
datasource db {
|
|
12
|
+
provider = "mysql"
|
|
13
|
+
url = env("DATABASE_URL")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
model User {
|
|
17
|
+
id Int @id @default(autoincrement())
|
|
18
|
+
name String
|
|
19
|
+
email String @unique
|
|
20
|
+
password String
|
|
21
|
+
isDeleted Boolean @default(false)
|
|
22
|
+
createdAt DateTime @default(now())
|
|
23
|
+
updatedAt DateTime @updatedAt
|
|
24
|
+
|
|
25
|
+
@@index([email, createdAt])
|
|
26
|
+
@@map("users")
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NestFactory } from '@nestjs/core';
|
|
2
|
+
import { AppModule } from './app.module';
|
|
3
|
+
import { ValidationPipe } from './common/pipes/validation.pipe';
|
|
4
|
+
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
|
5
|
+
import { checkEnvVariables } from './common/utils/helper';
|
|
6
|
+
import { REQ_ENV } from './common/utils/constants';
|
|
7
|
+
|
|
8
|
+
async function bootstrap() {
|
|
9
|
+
checkEnvVariables(REQ_ENV);
|
|
10
|
+
const app = await NestFactory.create(AppModule);
|
|
11
|
+
app.enableCors({
|
|
12
|
+
origin: '*',
|
|
13
|
+
methods: 'GET,PUT,POST,PATCH,DELETE',
|
|
14
|
+
credentials: true
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
app.useGlobalPipes(new ValidationPipe());
|
|
18
|
+
|
|
19
|
+
app.useGlobalFilters(new HttpExceptionFilter());
|
|
20
|
+
|
|
21
|
+
const port = process.env.PORT ?? 5000;
|
|
22
|
+
await app.listen(port, () => {
|
|
23
|
+
console.log(`🚀 Server is listening on port ${port}`);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
bootstrap();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Body, Controller, Get, Post, Put } from '@nestjs/common';
|
|
2
|
+
import { AuthService } from './auth.service';
|
|
3
|
+
import { LoginDto, RegisterDto, UpdateUserDto } from './dto';
|
|
4
|
+
import { CurrentUser } from 'src/common/decorators/user.decorator';
|
|
5
|
+
import { User } from '@prisma/client';
|
|
6
|
+
|
|
7
|
+
@Controller('auth')
|
|
8
|
+
export class AuthController {
|
|
9
|
+
constructor(private readonly authService: AuthService) {}
|
|
10
|
+
|
|
11
|
+
@Post('register')
|
|
12
|
+
register(@Body() body: RegisterDto) {
|
|
13
|
+
return this.authService.register(body);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Post('login')
|
|
17
|
+
login(@Body() body: LoginDto) {
|
|
18
|
+
return this.authService.login(body);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Get('profile')
|
|
22
|
+
profile(@CurrentUser() user: Partial<User>) {
|
|
23
|
+
return this.authService.profile(user);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Put('update')
|
|
27
|
+
update(@Body() body: UpdateUserDto, @CurrentUser() user: Partial<User>) {
|
|
28
|
+
return this.authService.update(body, user.id as number);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { AuthController } from './auth.controller';
|
|
3
|
+
import { AuthService } from './auth.service';
|
|
4
|
+
|
|
5
|
+
@Module({
|
|
6
|
+
controllers: [AuthController],
|
|
7
|
+
providers: [AuthService],
|
|
8
|
+
exports: [AuthService]
|
|
9
|
+
})
|
|
10
|
+
export class AuthModule {}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { PrismaService } from 'src/core/prisma/prisma.service';
|
|
3
|
+
import { LoginDto, LoginResDto, RegisterDto, UpdateUserDto } from './dto';
|
|
4
|
+
import {
|
|
5
|
+
comparePassword,
|
|
6
|
+
encryptPassword,
|
|
7
|
+
successResponse,
|
|
8
|
+
successResponseWithResult
|
|
9
|
+
} from 'src/common/utils/helper';
|
|
10
|
+
import {
|
|
11
|
+
ERROR_MSG,
|
|
12
|
+
RESOURCE_NAME,
|
|
13
|
+
SUCCESS_MSG
|
|
14
|
+
} from 'src/common/utils/constants';
|
|
15
|
+
import {
|
|
16
|
+
CustomException,
|
|
17
|
+
ResourceNotFound
|
|
18
|
+
} from 'src/common/exceptions/custom.exception';
|
|
19
|
+
import { ResponseDto } from 'src/common/dtos/common.dto';
|
|
20
|
+
import { JwtService } from 'src/core/jwt/jwt.service';
|
|
21
|
+
import { User } from '@prisma/client';
|
|
22
|
+
|
|
23
|
+
@Injectable()
|
|
24
|
+
export class AuthService {
|
|
25
|
+
constructor(
|
|
26
|
+
private readonly prisma: PrismaService,
|
|
27
|
+
private readonly jwtService: JwtService
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
async register(body: RegisterDto): Promise<ResponseDto> {
|
|
31
|
+
const existingUser = await this.findByEmail(body.email);
|
|
32
|
+
if (existingUser) throw new CustomException(ERROR_MSG.EMAIL_EXISTS);
|
|
33
|
+
|
|
34
|
+
body.password = await encryptPassword(body.password);
|
|
35
|
+
await this.prisma.user.create({ data: body });
|
|
36
|
+
|
|
37
|
+
return successResponse(SUCCESS_MSG.REGISTERED);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async login(body: LoginDto): Promise<LoginResDto> {
|
|
41
|
+
const user = await this.findByEmail(body.email);
|
|
42
|
+
if (!user) throw new ResourceNotFound(RESOURCE_NAME.USER);
|
|
43
|
+
|
|
44
|
+
if (!(await comparePassword(body.password, user.password)))
|
|
45
|
+
throw new CustomException(ERROR_MSG.INVALID_PASSWORD);
|
|
46
|
+
|
|
47
|
+
const token = await this.jwtService.signAsync({ id: user.id });
|
|
48
|
+
return successResponseWithResult(SUCCESS_MSG.LOG_IN, { token });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
profile(user: Partial<User>) {
|
|
52
|
+
return successResponseWithResult(SUCCESS_MSG.FETCHED, user);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async verifyToken(token: string): Promise<Partial<User>> {
|
|
56
|
+
const { id } = await this.jwtService.verifyAsync(token);
|
|
57
|
+
return this.findById(id);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async update(data: UpdateUserDto, id: number): Promise<ResponseDto> {
|
|
61
|
+
await this.findById(id);
|
|
62
|
+
|
|
63
|
+
const existingUser = await this.findByEmail(data.email);
|
|
64
|
+
if (existingUser && existingUser.id !== id)
|
|
65
|
+
throw new CustomException(ERROR_MSG.EMAIL_EXISTS);
|
|
66
|
+
|
|
67
|
+
await this.prisma.user.update({ where: { id }, data });
|
|
68
|
+
return successResponse(SUCCESS_MSG.UPDATED);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private async findById(id: number): Promise<Partial<User>> {
|
|
72
|
+
const user = await this.prisma.user.findUnique({
|
|
73
|
+
where: { id, isDeleted: false },
|
|
74
|
+
select: { id: true, name: true, email: true }
|
|
75
|
+
});
|
|
76
|
+
if (!user) throw new ResourceNotFound(RESOURCE_NAME.USER);
|
|
77
|
+
return user;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async findByEmail(email: string): Promise<User | null> {
|
|
81
|
+
return this.prisma.user.findUnique({ where: { email } });
|
|
82
|
+
}
|
|
83
|
+
}
|