251119:1700 Backend Phase 1
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Contract } from './contract.entity.js';
|
||||
import { Organization } from './organization.entity.js';
|
||||
|
||||
@Entity('contract_organizations')
|
||||
export class ContractOrganization {
|
||||
@PrimaryColumn({ name: 'contract_id' })
|
||||
contractId!: number;
|
||||
|
||||
@PrimaryColumn({ name: 'organization_id' })
|
||||
organizationId!: number;
|
||||
|
||||
@Column({ name: 'role_in_contract', nullable: true, length: 100 })
|
||||
roleInContract?: string;
|
||||
|
||||
// Relation ไปยัง Contract
|
||||
@ManyToOne(() => Contract, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'contract_id' })
|
||||
contract?: Contract;
|
||||
|
||||
// Relation ไปยัง Organization
|
||||
@ManyToOne(() => Organization, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'organization_id' })
|
||||
organization?: Organization;
|
||||
}
|
||||
41
backend/src/modules/project/entities/contract.entity.ts
Normal file
41
backend/src/modules/project/entities/contract.entity.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity.js';
|
||||
import { Project } from './project.entity.js';
|
||||
|
||||
@Entity('contracts')
|
||||
export class Contract extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'project_id' })
|
||||
projectId!: number;
|
||||
|
||||
@Column({ name: 'contract_code', unique: true, length: 50 })
|
||||
contractCode!: string;
|
||||
|
||||
@Column({ name: 'contract_name', length: 255 })
|
||||
contractName!: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Column({ name: 'start_date', type: 'date', nullable: true })
|
||||
startDate?: Date;
|
||||
|
||||
@Column({ name: 'end_date', type: 'date', nullable: true })
|
||||
endDate?: Date;
|
||||
|
||||
@Column({ name: 'is_active', default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
// Relation
|
||||
@ManyToOne(() => Project)
|
||||
@JoinColumn({ name: 'project_id' })
|
||||
project?: Project;
|
||||
}
|
||||
17
backend/src/modules/project/entities/organization.entity.ts
Normal file
17
backend/src/modules/project/entities/organization.entity.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity.js';
|
||||
|
||||
@Entity('organizations')
|
||||
export class Organization extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'organization_code', unique: true, length: 20 })
|
||||
organizationCode!: string;
|
||||
|
||||
@Column({ name: 'organization_name', length: 255 })
|
||||
organizationName!: string;
|
||||
|
||||
@Column({ name: 'is_active', default: true })
|
||||
isActive!: boolean;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Entity, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Project } from './project.entity.js';
|
||||
import { Organization } from './organization.entity.js';
|
||||
|
||||
@Entity('project_organizations')
|
||||
export class ProjectOrganization {
|
||||
// Composite Primary Key (ใช้ 2 คอลัมน์รวมกันเป็น PK)
|
||||
@PrimaryColumn({ name: 'project_id' })
|
||||
projectId!: number;
|
||||
|
||||
@PrimaryColumn({ name: 'organization_id' })
|
||||
organizationId!: number;
|
||||
|
||||
// Relation ไปยัง Project
|
||||
@ManyToOne(() => Project, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'project_id' })
|
||||
project?: Project;
|
||||
|
||||
// Relation ไปยัง Organization
|
||||
@ManyToOne(() => Organization, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'organization_id' })
|
||||
organization?: Organization;
|
||||
}
|
||||
17
backend/src/modules/project/entities/project.entity.ts
Normal file
17
backend/src/modules/project/entities/project.entity.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity.js';
|
||||
|
||||
@Entity('projects')
|
||||
export class Project extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'project_code', unique: true, length: 50 })
|
||||
projectCode!: string;
|
||||
|
||||
@Column({ name: 'project_name', length: 255 })
|
||||
projectName!: string;
|
||||
|
||||
@Column({ name: 'is_active', default: 1, type: 'tinyint' })
|
||||
isActive!: boolean;
|
||||
}
|
||||
19
backend/src/modules/project/project.controller.spec.ts
Normal file
19
backend/src/modules/project/project.controller.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { ProjectService } from './project.service.js';
|
||||
import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
|
||||
|
||||
@Controller('projects')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class ProjectController {
|
||||
constructor(private readonly projectService: ProjectService) {}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.projectService.findAllProjects();
|
||||
}
|
||||
|
||||
@Get('organizations')
|
||||
findAllOrgs() {
|
||||
return this.projectService.findAllOrganizations();
|
||||
}
|
||||
}
|
||||
4
backend/src/modules/project/project.controller.ts
Normal file
4
backend/src/modules/project/project.controller.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Controller } from '@nestjs/common';
|
||||
|
||||
@Controller('project')
|
||||
export class ProjectController {}
|
||||
25
backend/src/modules/project/project.module.ts
Normal file
25
backend/src/modules/project/project.module.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ProjectService } from './project.service.js';
|
||||
import { ProjectController } from './project.controller.js';
|
||||
import { Project } from './entities/project.entity.js';
|
||||
import { Organization } from './entities/organization.entity.js';
|
||||
import { Contract } from './entities/contract.entity.js';
|
||||
import { ProjectOrganization } from './entities/project-organization.entity.js'; // เพิ่ม
|
||||
import { ContractOrganization } from './entities/contract-organization.entity.js'; // เพิ่ม
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
Project,
|
||||
Organization,
|
||||
Contract,
|
||||
ProjectOrganization, // ลงทะเบียน
|
||||
ContractOrganization, // ลงทะเบียน
|
||||
]),
|
||||
],
|
||||
controllers: [ProjectController],
|
||||
providers: [ProjectService],
|
||||
exports: [ProjectService], // Export เผื่อ Module อื่นใช้
|
||||
})
|
||||
export class ProjectModule {}
|
||||
18
backend/src/modules/project/project.service.spec.ts
Normal file
18
backend/src/modules/project/project.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ProjectService } from './project.service';
|
||||
|
||||
describe('ProjectService', () => {
|
||||
let service: ProjectService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [ProjectService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ProjectService>(ProjectService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
25
backend/src/modules/project/project.service.ts
Normal file
25
backend/src/modules/project/project.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Project } from './entities/project.entity.js';
|
||||
import { Organization } from './entities/organization.entity.js';
|
||||
|
||||
@Injectable()
|
||||
export class ProjectService {
|
||||
constructor(
|
||||
@InjectRepository(Project)
|
||||
private projectRepository: Repository<Project>,
|
||||
@InjectRepository(Organization)
|
||||
private organizationRepository: Repository<Organization>,
|
||||
) {}
|
||||
|
||||
// ดึงรายการ Project ทั้งหมด
|
||||
async findAllProjects() {
|
||||
return this.projectRepository.find();
|
||||
}
|
||||
|
||||
// ดึงรายการ Organization ทั้งหมด
|
||||
async findAllOrganizations() {
|
||||
return this.organizationRepository.find();
|
||||
}
|
||||
}
|
||||
44
backend/src/modules/user/dto/create-user.dto.ts
Normal file
44
backend/src/modules/user/dto/create-user.dto.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
IsString,
|
||||
IsEmail,
|
||||
IsNotEmpty,
|
||||
MinLength,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateUserDto {
|
||||
@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;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
lineId?: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
primaryOrganizationId?: number; // รับเป็น ID ของ Organization
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isActive?: boolean;
|
||||
}
|
||||
4
backend/src/modules/user/dto/update-user.dto.ts
Normal file
4
backend/src/modules/user/dto/update-user.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateUserDto } from './create-user.dto.js';
|
||||
|
||||
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||
53
backend/src/modules/user/entities/user.entity.ts
Normal file
53
backend/src/modules/user/entities/user.entity.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
ManyToOne, // <--- เพิ่มตรงนี้
|
||||
JoinColumn, // <--- เพิ่มตรงนี้
|
||||
} from 'typeorm';
|
||||
import { Organization } from '../../project/entities/organization.entity.js'; // อย่าลืม import Organization
|
||||
|
||||
@Entity('users')
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn({ name: 'user_id' })
|
||||
user_id!: number;
|
||||
|
||||
@Column({ unique: true, length: 50 })
|
||||
username!: string;
|
||||
|
||||
@Column({ name: 'password_hash' })
|
||||
password!: string;
|
||||
|
||||
@Column({ unique: true, length: 100 })
|
||||
email!: string;
|
||||
|
||||
@Column({ name: 'first_name', nullable: true, length: 50 })
|
||||
firstName?: string;
|
||||
|
||||
@Column({ name: 'last_name', nullable: true, length: 50 })
|
||||
lastName?: string;
|
||||
|
||||
@Column({ name: 'is_active', default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
// Relation กับ Organization
|
||||
@Column({ name: 'primary_organization_id', nullable: true })
|
||||
primaryOrganizationId?: number;
|
||||
|
||||
@ManyToOne(() => Organization)
|
||||
@JoinColumn({ name: 'primary_organization_id' })
|
||||
organization?: Organization;
|
||||
|
||||
// Base Entity Fields (ที่เราแยกมาเขียนเองเพราะเรื่อง deleted_at)
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', select: false })
|
||||
deletedAt?: Date;
|
||||
}
|
||||
57
backend/src/modules/user/user.controller.ts
Normal file
57
backend/src/modules/user/user.controller.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { UserService } from './user.service.js';
|
||||
import { CreateUserDto } from './dto/create-user.dto.js';
|
||||
import { UpdateUserDto } from './dto/update-user.dto.js';
|
||||
import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
import { RbacGuard } from '../../common/auth/rbac.guard.js';
|
||||
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard) // 🔒 เพิ่ม RbacGuard ต่อท้าย) // 🔒 บังคับ Login ทุก Endpoints ในนี้
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
// 1. สร้างผู้ใช้ใหม่
|
||||
@Post()
|
||||
@RequirePermission('user.create') // 🔒 ต้องมีสิทธิ์ user.create ถึงจะเข้าได้
|
||||
create(@Body() createUserDto: CreateUserDto) {
|
||||
return this.userService.create(createUserDto);
|
||||
}
|
||||
|
||||
// 2. ดูรายชื่อผู้ใช้ทั้งหมด
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.userService.findAll();
|
||||
}
|
||||
|
||||
// 3. ดูข้อมูลผู้ใช้รายคน (ตาม ID)
|
||||
@Get(':id')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.userService.findOne(id);
|
||||
}
|
||||
|
||||
// 4. แก้ไขข้อมูลผู้ใช้
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() updateUserDto: UpdateUserDto,
|
||||
) {
|
||||
return this.userService.update(id, updateUserDto);
|
||||
}
|
||||
|
||||
// 5. ลบผู้ใช้ (Soft Delete)
|
||||
@Delete(':id')
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.userService.remove(id);
|
||||
}
|
||||
}
|
||||
14
backend/src/modules/user/user.module.ts
Normal file
14
backend/src/modules/user/user.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserService } from './user.service.js';
|
||||
import { UserController } from './user.controller.js'; // 1. Import Controller
|
||||
import { User } from './entities/user.entity.js';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])], // จดทะเบียน Entity
|
||||
// 2. เพิ่มบรรทัดนี้ เพื่อบอก NestJS ว่ามี Controller นี้อยู่
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
exports: [UserService], // Export ให้ AuthModule เรียกใช้ได้
|
||||
})
|
||||
export class UserModule {}
|
||||
18
backend/src/modules/user/user.service.spec.ts
Normal file
18
backend/src/modules/user/user.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UserService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UserService>(UserService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
119
backend/src/modules/user/user.service.ts
Normal file
119
backend/src/modules/user/user.service.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { User } from './entities/user.entity.js';
|
||||
import { CreateUserDto } from './dto/create-user.dto.js';
|
||||
import { UpdateUserDto } from './dto/update-user.dto.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private usersRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
// 1. สร้างผู้ใช้ (Hash Password ก่อนบันทึก)
|
||||
async create(createUserDto: CreateUserDto): Promise<User> {
|
||||
// สร้าง Salt และ Hash Password
|
||||
const salt = await bcrypt.genSalt();
|
||||
const hashedPassword = await bcrypt.hash(createUserDto.password, salt);
|
||||
|
||||
// เตรียมข้อมูล (เปลี่ยน password ธรรมดา เป็น password_hash)
|
||||
const newUser = this.usersRepository.create({
|
||||
...createUserDto,
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
try {
|
||||
// บันทึกลง DB
|
||||
return await this.usersRepository.save(newUser);
|
||||
} catch (error: any) {
|
||||
// เช็ค Error กรณี Username/Email ซ้ำ (MySQL Error Code 1062)
|
||||
if (error.code === 'ER_DUP_ENTRY') {
|
||||
throw new ConflictException('Username or Email already exists');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. ดึงข้อมูลทั้งหมด
|
||||
async findAll(): Promise<User[]> {
|
||||
return this.usersRepository.find({
|
||||
// ไม่ส่ง password กลับไปเพื่อความปลอดภัย
|
||||
select: [
|
||||
'user_id',
|
||||
'username',
|
||||
'email',
|
||||
'firstName',
|
||||
'lastName',
|
||||
'isActive',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 3. ดึงข้อมูลรายคน
|
||||
async findOne(id: number): Promise<User> {
|
||||
const user = await this.usersRepository.findOne({
|
||||
where: { user_id: id }, // ใช้ user_id ตามที่คุณตั้งชื่อไว้
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// ฟังก์ชันแถม: สำหรับ AuthService ใช้ (ต้องเห็น Password เพื่อเอาไปเทียบ)
|
||||
async findOneByUsername(username: string): Promise<User | null> {
|
||||
return this.usersRepository.findOne({ where: { username } });
|
||||
}
|
||||
|
||||
// 4. แก้ไขข้อมูล
|
||||
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
|
||||
// เช็คก่อนว่ามี User นี้ไหม
|
||||
const user = await this.findOne(id);
|
||||
|
||||
// ถ้ามีการแก้รหัสผ่าน ต้อง Hash ใหม่ด้วย
|
||||
if (updateUserDto.password) {
|
||||
const salt = await bcrypt.genSalt();
|
||||
updateUserDto.password = await bcrypt.hash(updateUserDto.password, salt);
|
||||
}
|
||||
|
||||
// รวมร่างข้อมูลเดิม + ข้อมูลใหม่
|
||||
const updatedUser = this.usersRepository.merge(user, updateUserDto);
|
||||
|
||||
return this.usersRepository.save(updatedUser);
|
||||
}
|
||||
|
||||
// 5. ลบผู้ใช้ (Soft Delete)
|
||||
async remove(id: number): Promise<void> {
|
||||
const result = await this.usersRepository.softDelete(id);
|
||||
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`User with ID ${id} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
// 👇👇 เพิ่มฟังก์ชันใหม่นี้ 👇👇
|
||||
async getUserPermissions(userId: number): Promise<string[]> {
|
||||
// Query ข้อมูลจาก View: v_user_all_permissions (ที่เราสร้างไว้ใน SQL Script)
|
||||
// เนื่องจาก TypeORM ไม่รองรับ View โดยตรงในบางท่า เราใช้ query builder หรือ query raw ได้
|
||||
// แต่เพื่อความง่ายและประสิทธิภาพ เราจะใช้ query raw ครับ
|
||||
|
||||
const permissions = await this.usersRepository.query(
|
||||
`SELECT permission_name FROM v_user_all_permissions WHERE user_id = ?`,
|
||||
[userId],
|
||||
);
|
||||
|
||||
// แปลงผลลัพธ์เป็น Array ของ string ['user.create', 'project.view', ...]
|
||||
return permissions.map((row: any) => row.permission_name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user