251119:1700 Backend Phase 1
This commit is contained in:
18
backend/src/common/aut/aut.controller.spec.ts
Normal file
18
backend/src/common/aut/aut.controller.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AutController } from './aut.controller';
|
||||
|
||||
describe('AutController', () => {
|
||||
let controller: AutController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AutController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<AutController>(AutController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
4
backend/src/common/aut/aut.controller.ts
Normal file
4
backend/src/common/aut/aut.controller.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Controller } from '@nestjs/common';
|
||||
|
||||
@Controller('aut')
|
||||
export class AutController {}
|
||||
30
backend/src/common/auth/auth.controller.spec.ts
Normal file
30
backend/src/common/auth/auth.controller.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { LoginDto } from './dto/login.dto.js'; // <--- Import DTO
|
||||
import { RegisterDto } from './dto/register.dto.js'; // <--- Import DTO
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@Post('login')
|
||||
// เปลี่ยน @Body() req เป็น @Body() loginDto: LoginDto
|
||||
async login(@Body() loginDto: LoginDto) {
|
||||
const user = await this.authService.validateUser(
|
||||
loginDto.username,
|
||||
loginDto.password,
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
return this.authService.login(user);
|
||||
}
|
||||
|
||||
@Post('register-admin')
|
||||
// เปลี่ยน @Body() req เป็น @Body() registerDto: RegisterDto
|
||||
async register(@Body() registerDto: RegisterDto) {
|
||||
return this.authService.register(registerDto);
|
||||
}
|
||||
}
|
||||
30
backend/src/common/auth/auth.controller.ts
Normal file
30
backend/src/common/auth/auth.controller.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { LoginDto } from './dto/login.dto.js'; // <--- Import DTO
|
||||
import { RegisterDto } from './dto/register.dto.js'; // <--- Import DTO
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@Post('login')
|
||||
// เปลี่ยน @Body() req เป็น @Body() loginDto: LoginDto
|
||||
async login(@Body() loginDto: LoginDto) {
|
||||
const user = await this.authService.validateUser(
|
||||
loginDto.username,
|
||||
loginDto.password,
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
return this.authService.login(user);
|
||||
}
|
||||
|
||||
@Post('register-admin')
|
||||
// เปลี่ยน @Body() req เป็น @Body() registerDto: RegisterDto
|
||||
async register(@Body() registerDto: RegisterDto) {
|
||||
return this.authService.register(registerDto);
|
||||
}
|
||||
}
|
||||
31
backend/src/common/auth/auth.module.ts
Normal file
31
backend/src/common/auth/auth.module.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { AuthController } from './auth.controller.js';
|
||||
import { UserModule } from '../../modules/user/user.module.js';
|
||||
import { JwtStrategy } from './jwt.strategy.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UserModule,
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get<string>('JWT_SECRET'),
|
||||
signOptions: {
|
||||
// Cast เป็น any เพื่อแก้ปัญหา Type ไม่ตรงกับ Library
|
||||
expiresIn: (configService.get<string>('JWT_EXPIRATION') ||
|
||||
'8h') as any,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
controllers: [AuthController],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
18
backend/src/common/auth/auth.service.spec.ts
Normal file
18
backend/src/common/auth/auth.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AuthService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
41
backend/src/common/auth/auth.service.ts
Normal file
41
backend/src/common/auth/auth.service.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { UserService } from '../../modules/user/user.service.js';
|
||||
import { RegisterDto } from './dto/register.dto.js'; // Import DTO
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async validateUser(username: string, pass: string): Promise<any> {
|
||||
const user = await this.userService.findOneByUsername(username);
|
||||
if (user && (await bcrypt.compare(pass, user.password))) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { password, ...result } = user;
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async login(user: any) {
|
||||
const payload = { username: user.username, sub: user.user_id };
|
||||
return {
|
||||
access_token: this.jwtService.sign(payload),
|
||||
};
|
||||
}
|
||||
|
||||
async register(userDto: RegisterDto) {
|
||||
const salt = await bcrypt.genSalt();
|
||||
const hashedPassword = await bcrypt.hash(userDto.password, salt);
|
||||
|
||||
// ใช้ค่าจาก DTO ที่ Validate มาแล้ว
|
||||
return this.userService.create({
|
||||
...userDto,
|
||||
password: hashedPassword,
|
||||
});
|
||||
}
|
||||
}
|
||||
11
backend/src/common/auth/dto/login.dto.ts
Normal file
11
backend/src/common/auth/dto/login.dto.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class LoginDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
username!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password!: string;
|
||||
}
|
||||
30
backend/src/common/auth/dto/register.dto.ts
Normal file
30
backend/src/common/auth/dto/register.dto.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
IsEmail,
|
||||
IsNotEmpty,
|
||||
IsString,
|
||||
MinLength,
|
||||
IsOptional,
|
||||
} from 'class-validator';
|
||||
|
||||
export class RegisterDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
username!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MinLength(6, { message: 'Password must be at least 6 characters' })
|
||||
password!: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
firstName?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
lastName?: string;
|
||||
}
|
||||
5
backend/src/common/auth/jwt-auth.guard.ts
Normal file
5
backend/src/common/auth/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') {}
|
||||
26
backend/src/common/auth/jwt.strategy.ts
Normal file
26
backend/src/common/auth/jwt.strategy.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
// Interface สำหรับ Payload ใน Token
|
||||
interface JwtPayload {
|
||||
sub: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(configService: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
// ใส่ ! เพื่อยืนยันว่ามีค่าแน่นอน (ConfigValidation เช็คให้แล้ว)
|
||||
secretOrKey: configService.get<string>('JWT_SECRET')!,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload) {
|
||||
return { userId: payload.sub, username: payload.username };
|
||||
}
|
||||
}
|
||||
55
backend/src/common/auth/rbac.guard.ts
Normal file
55
backend/src/common/auth/rbac.guard.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { PERMISSION_KEY } from '../decorators/require-permission.decorator.js';
|
||||
import { UserService } from '../../modules/user/user.service.js';
|
||||
|
||||
@Injectable()
|
||||
export class RbacGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private userService: UserService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
// 1. ดูว่า Controller นี้ต้องการสิทธิ์อะไร?
|
||||
const requiredPermission = this.reflector.getAllAndOverride<string>(
|
||||
PERMISSION_KEY,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
|
||||
// ถ้าไม่ต้องการสิทธิ์อะไรเลย ก็ปล่อยผ่าน
|
||||
if (!requiredPermission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. ดึง User จาก Request (ที่ JwtAuthGuard แปะไว้ให้)
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
if (!user) {
|
||||
throw new ForbiddenException('User not found in request');
|
||||
}
|
||||
|
||||
// 3. (สำคัญ) ดึงสิทธิ์ทั้งหมดของ User คนนี้จาก Database
|
||||
// เราต้องเขียนฟังก์ชัน getUserPermissions ใน UserService เพิ่ม (เดี๋ยวพาทำ)
|
||||
const userPermissions = await this.userService.getUserPermissions(
|
||||
user.userId,
|
||||
);
|
||||
|
||||
// 4. ตรวจสอบว่ามีสิทธิ์ที่ต้องการไหม?
|
||||
const hasPermission = userPermissions.some(
|
||||
(p) => p === requiredPermission || p === 'system.manage_all', // Superadmin ทะลุทุกสิทธิ์
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new ForbiddenException(
|
||||
`You do not have permission: ${requiredPermission}`,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
9
backend/src/common/common.module.ts
Normal file
9
backend/src/common/common.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { AutController } from './aut/aut.controller';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule],
|
||||
controllers: [AutController]
|
||||
})
|
||||
export class CommonModule {}
|
||||
31
backend/src/common/config/env.validation.ts
Normal file
31
backend/src/common/config/env.validation.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// File: src/common/config/env.validation.ts
|
||||
import Joi from 'joi';
|
||||
|
||||
// สร้าง Schema สำหรับตรวจสอบค่า Environment Variables
|
||||
export const envValidationSchema = Joi.object({
|
||||
// 1. Application Environment
|
||||
NODE_ENV: Joi.string()
|
||||
.valid('development', 'production', 'test', 'provision')
|
||||
.default('development'),
|
||||
PORT: Joi.number().default(3000),
|
||||
|
||||
// 2. Database Configuration (MariaDB)
|
||||
// ห้ามเป็นค่าว่าง (required)
|
||||
DB_HOST: Joi.string().required(),
|
||||
DB_PORT: Joi.number().default(3306),
|
||||
DB_USERNAME: Joi.string().required(),
|
||||
DB_PASSWORD: Joi.string().required(),
|
||||
DB_DATABASE: Joi.string().required(),
|
||||
|
||||
// 3. Security (JWT)
|
||||
// ต้องมีค่า และควรยาวพอ (ตรวจสอบความยาวได้ถ้าระบุ min)
|
||||
JWT_SECRET: Joi.string()
|
||||
.required()
|
||||
.min(32)
|
||||
.message('JWT_SECRET must be at least 32 characters long for security.'),
|
||||
JWT_EXPIRATION: Joi.string().default('8h'),
|
||||
// 4. Redis Configuration (เพิ่มส่วนนี้)
|
||||
REDIS_HOST: Joi.string().required(),
|
||||
REDIS_PORT: Joi.number().default(6379),
|
||||
REDIS_PASSWORD: Joi.string().required(),
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const PERMISSION_KEY = 'permissions';
|
||||
|
||||
// ใช้สำหรับแปะหน้า Controller/Method
|
||||
// ตัวอย่าง: @RequirePermission('user.create')
|
||||
export const RequirePermission = (permission: string) =>
|
||||
SetMetadata(PERMISSION_KEY, permission);
|
||||
20
backend/src/common/entities/base.entity.ts
Normal file
20
backend/src/common/entities/base.entity.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
export abstract class BaseEntity {
|
||||
// @PrimaryGeneratedColumn()
|
||||
// id!: number;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
created_at!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updated_at!: Date;
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', select: false }) // select: false เพื่อซ่อน field นี้โดย Default
|
||||
deleted_at!: Date;
|
||||
}
|
||||
50
backend/src/common/exceptions/http-exception.filter.ts
Normal file
50
backend/src/common/exceptions/http-exception.filter.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
@Catch()
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(HttpExceptionFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const exceptionResponse =
|
||||
exception instanceof HttpException
|
||||
? exception.getResponse()
|
||||
: 'Internal server error';
|
||||
|
||||
// จัดรูปแบบ Error Message
|
||||
const message =
|
||||
typeof exceptionResponse === 'string'
|
||||
? exceptionResponse
|
||||
: (exceptionResponse as any).message || exceptionResponse;
|
||||
// 👇👇 เพิ่มบรรทัดนี้ครับ (สำคัญมาก!) 👇👇
|
||||
console.error('💥 REAL ERROR:', exception);
|
||||
|
||||
// Log Error (สำคัญมากสำหรับการ Debug แต่ไม่ส่งให้ Client เห็นทั้งหมด)
|
||||
this.logger.error(
|
||||
`Http Status: ${status} Error Message: ${JSON.stringify(message)}`,
|
||||
);
|
||||
|
||||
response.status(status).json({
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
message: status === 500 ? 'Internal server error' : message, // ซ่อน Detail กรณี 500
|
||||
});
|
||||
}
|
||||
}
|
||||
32
backend/src/common/interceptors/transform.interceptor.ts
Normal file
32
backend/src/common/interceptors/transform.interceptor.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
export interface Response<T> {
|
||||
statusCode: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TransformInterceptor<T>
|
||||
implements NestInterceptor<T, Response<T>>
|
||||
{
|
||||
intercept(
|
||||
context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<Response<T>> {
|
||||
return next.handle().pipe(
|
||||
map((data) => ({
|
||||
statusCode: context.switchToHttp().getResponse().statusCode,
|
||||
message: data?.message || 'Success', // ถ้า data มี message ให้ใช้ ถ้าไม่มีใช้ 'Success'
|
||||
data: data?.result || data, // รองรับกรณีส่ง object ที่มี key result มา
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user