251123:2300 Update T1

This commit is contained in:
2025-11-24 08:15:15 +07:00
parent 23006898d9
commit 9360d78ea6
81 changed files with 4232 additions and 347 deletions
@@ -0,0 +1,42 @@
// File: src/modules/user/dto/update-preference.dto.ts
// บันทึกการแก้ไข: DTO สำหรับตรวจสอบข้อมูลการอัปเดต User Preferences (T1.3)
import { IsBoolean, IsOptional, IsString, IsIn } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger'; // ใช้สำหรับสร้าง API Documentation (Swagger)
export class UpdatePreferenceDto {
@ApiPropertyOptional({
description: 'รับการแจ้งเตือนทางอีเมลหรือไม่',
default: true,
})
@IsOptional()
@IsBoolean()
notifyEmail?: boolean;
@ApiPropertyOptional({
description: 'รับการแจ้งเตือนทาง LINE หรือไม่',
default: true,
})
@IsOptional()
@IsBoolean()
notifyLine?: boolean;
@ApiPropertyOptional({
description:
'รับการแจ้งเตือนแบบรวม (Digest) แทน Real-time เพื่อลดจำนวนข้อความ',
default: false,
})
@IsOptional()
@IsBoolean()
digestMode?: boolean;
@ApiPropertyOptional({
description: 'ธีมของหน้าจอ (light, dark, หรือ system)',
default: 'light',
enum: ['light', 'dark', 'system'],
})
@IsOptional()
@IsString()
@IsIn(['light', 'dark', 'system']) // บังคับว่าต้องเป็นค่าใดค่าหนึ่งในนี้เท่านั้น
uiTheme?: string;
}
@@ -1,4 +1,21 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto.js';
// File: src/modules/user/dto/update-preference.dto.ts
import { IsBoolean, IsOptional, IsString, IsIn } from 'class-validator';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
export class UpdatePreferenceDto {
@IsOptional()
@IsBoolean()
notifyEmail?: boolean;
@IsOptional()
@IsBoolean()
notifyLine?: boolean;
@IsOptional()
@IsBoolean()
digestMode?: boolean;
@IsOptional()
@IsString()
@IsIn(['light', 'dark', 'system'])
uiTheme?: string;
}
@@ -0,0 +1,27 @@
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('permissions')
export class Permission {
@PrimaryGeneratedColumn({ name: 'permission_id' })
permissionId!: number;
@Column({ name: 'permission_name', length: 100, unique: true })
permissionName!: string; // e.g., 'rfa.create'
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ length: 50, nullable: true })
module?: string; // e.g., 'rfa', 'user'
@Column({
name: 'scope_level',
type: 'enum',
enum: ['GLOBAL', 'ORG', 'PROJECT'],
nullable: true,
})
scopeLevel?: string;
@Column({ name: 'is_active', default: true })
isActive!: boolean;
}
@@ -0,0 +1,29 @@
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
export enum RoleScope {
GLOBAL = 'Global',
ORGANIZATION = 'Organization',
PROJECT = 'Project',
CONTRACT = 'Contract',
}
@Entity('roles')
export class Role {
@PrimaryGeneratedColumn({ name: 'role_id' })
roleId!: number;
@Column({ name: 'role_name', length: 100 })
roleName!: string;
@Column({
type: 'enum',
enum: RoleScope,
})
scope!: RoleScope;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ name: 'is_system', default: false })
isSystem!: boolean;
}
@@ -1,3 +1,6 @@
// File: src/modules/user/entities/user-assignment.entity.ts
// บันทึกการแก้ไข: Entity สำหรับการมอบหมาย Role ให้กับ User ตาม Scope (T1.3, RBAC 4-Level)
import {
Entity,
Column,
@@ -6,8 +9,11 @@ import {
JoinColumn,
CreateDateColumn,
} from 'typeorm';
import { User } from './user.entity.js';
// Import Role, Org, Project, Contract entities...
import { User } from './user.entity';
import { Role } from './role.entity';
import { Organization } from '../../project/entities/organization.entity'; // ปรับ Path ให้ตรงกับ ProjectModule
import { Project } from '../../project/entities/project.entity'; // ปรับ Path ให้ตรงกับ ProjectModule
import { Contract } from '../../project/entities/contract.entity'; // ปรับ Path ให้ตรงกับ ProjectModule
@Entity('user_assignments')
export class UserAssignment {
@@ -20,6 +26,7 @@ export class UserAssignment {
@Column({ name: 'role_id' })
roleId!: number;
// --- Scopes (เลือกได้เพียง 1 หรือเป็น NULL ทั้งหมดสำหรับ Global) ---
@Column({ name: 'organization_id', nullable: true })
organizationId?: number;
@@ -35,8 +42,29 @@ export class UserAssignment {
@CreateDateColumn({ name: 'assigned_at' })
assignedAt!: Date;
// Relation กลับไปหา User (เจ้าของสิทธิ์)
@ManyToOne(() => User)
// --- Relations ---
@ManyToOne(() => User, (user) => user.assignments, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user?: User;
user!: User;
@ManyToOne(() => Role, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'role_id' })
role!: Role;
@ManyToOne(() => Organization, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'organization_id' })
organization?: Organization;
@ManyToOne(() => Project, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'project_id' })
project?: Project;
@ManyToOne(() => Contract, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'contract_id' })
contract?: Contract;
@ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: 'assigned_by_user_id' })
assignedBy?: User;
}
@@ -1,7 +1,10 @@
// File: src/modules/user/entities/user-preference.entity.ts
// บันทึกการแก้ไข: Entity สำหรับเก็บการตั้งค่าส่วนตัวของผู้ใช้ แยกจากตาราง Users (T1.3)
import {
Entity,
PrimaryColumn,
Column,
PrimaryColumn,
UpdateDateColumn,
OneToOne,
JoinColumn,
@@ -10,7 +13,6 @@ import { User } from './user.entity';
@Entity('user_preferences')
export class UserPreference {
// ใช้ user_id เป็น Primary Key และ Foreign Key ในตัวเดียวกัน (1:1 Relation)
@PrimaryColumn({ name: 'user_id' })
userId!: number;
@@ -20,18 +22,17 @@ export class UserPreference {
@Column({ name: 'notify_line', default: true })
notifyLine!: boolean;
@Column({ name: 'digest_mode', default: true })
@Column({ name: 'digest_mode', default: false })
digestMode!: boolean; // รับแจ้งเตือนแบบรวม (Digest) แทน Real-time
@Column({ name: 'ui_theme', length: 20, default: 'light' })
@Column({ name: 'ui_theme', default: 'light', length: 20 })
uiTheme!: string;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date;
// --- Relations ---
@OneToOne(() => User)
// --- Relation ---
@OneToOne(() => User, (user) => user.preference, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user!: User;
}
@@ -1,3 +1,6 @@
// File: src/modules/user/entities/user.entity.ts
// บันทึกการแก้ไข: เพิ่ม Relations กับ UserAssignment และ UserPreference (T1.3)
import {
Entity,
Column,
@@ -5,10 +8,14 @@ import {
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
ManyToOne, // <--- เพิ่มตรงนี้
JoinColumn, // <--- เพิ่มตรงนี้
ManyToOne,
OneToMany,
OneToOne,
JoinColumn,
} from 'typeorm';
import { Organization } from '../../project/entities/organization.entity.js'; // อย่าลืม import Organization
import { Organization } from '../../project/entities/organization.entity'; // Adjust path as needed
import { UserAssignment } from './user-assignment.entity';
import { UserPreference } from './user-preference.entity';
@Entity('users')
export class User {
@@ -18,7 +25,7 @@ export class User {
@Column({ unique: true, length: 50 })
username!: string;
@Column({ name: 'password_hash' })
@Column({ name: 'password_hash', select: false }) // ไม่ Select Password โดย Default
password!: string;
@Column({ unique: true, length: 100 })
@@ -33,15 +40,26 @@ export class User {
@Column({ name: 'is_active', default: true })
isActive!: boolean;
// Relation กับ Organization
@Column({ name: 'line_id', nullable: true, length: 100 })
lineId?: string;
// Relation กับ Organization (สังกัดหลัก)
@Column({ name: 'primary_organization_id', nullable: true })
primaryOrganizationId?: number;
@ManyToOne(() => Organization)
@ManyToOne(() => Organization, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'primary_organization_id' })
organization?: Organization;
// Base Entity Fields (ที่เราแยกมาเขียนเองเพราะเรื่อง deleted_at)
// Relation กับ Assignments (RBAC)
@OneToMany(() => UserAssignment, (assignment) => assignment.user)
assignments?: UserAssignment[];
// Relation กับ Preferences (1:1)
@OneToOne(() => UserPreference, (pref) => pref.user, { cascade: true })
preference?: UserPreference;
// Base Entity Fields
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
@@ -0,0 +1,47 @@
// File: src/modules/user/user-preference.service.ts
// บันทึกการแก้ไข: Service จัดการการตั้งค่าส่วนตัว (T1.3)
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserPreference } from './entities/user-preference.entity';
import { UpdatePreferenceDto } from './dto/update-preference.dto';
@Injectable()
export class UserPreferenceService {
constructor(
@InjectRepository(UserPreference)
private prefRepo: Repository<UserPreference>,
) {}
// ดึง Preference ของ User (ถ้าไม่มีให้สร้าง Default)
async findByUser(userId: number): Promise<UserPreference> {
let pref = await this.prefRepo.findOne({ where: { userId } });
if (!pref) {
pref = this.prefRepo.create({
userId,
notifyEmail: true,
notifyLine: true,
digestMode: false,
uiTheme: 'light',
});
await this.prefRepo.save(pref);
}
return pref;
}
// อัปเดต Preference
async update(
userId: number,
dto: UpdatePreferenceDto,
): Promise<UserPreference> {
const pref = await this.findByUser(userId);
// Merge ข้อมูลใหม่
this.prefRepo.merge(pref, dto);
return this.prefRepo.save(pref);
}
}
+59 -20
View File
@@ -1,3 +1,6 @@
// File: src/modules/user/user.controller.ts
// บันทึกการแก้ไข: เพิ่ม Endpoints สำหรับ User Preferences (T1.3)
import {
Controller,
Get,
@@ -8,47 +11,86 @@ import {
Delete,
UseGuards,
ParseIntPipe,
Request, // <--- อย่าลืม Import Request
} 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 { AssignRoleDto } from './dto/assign-role.dto.js'; // <--- Import DTO
import { UserAssignmentService } from './user-assignment.service.js'; // <--- Import Service ใหม่
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
import { RbacGuard } from '../../common/guards/rbac.guard.js';
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
import { UserService } from './user.service';
import { UserAssignmentService } from './user-assignment.service';
import { UserPreferenceService } from './user-preference.service'; // ✅ เพิ่ม
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { AssignRoleDto } from './dto/assign-role.dto';
import { UpdatePreferenceDto } from './dto/update-preference.dto'; // ✅ เพิ่ม DTO
import { JwtAuthGuard } from '../../common/auth/guards/jwt-auth.guard';
import { RbacGuard } from '../../common/guards/rbac.guard'; // สมมติว่ามีแล้ว ถ้ายังไม่มีให้คอมเมนต์ไว้ก่อน
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { User } from './entities/user.entity';
@ApiTags('Users')
@ApiBearerAuth()
@Controller('users')
@UseGuards(JwtAuthGuard, RbacGuard)
@UseGuards(JwtAuthGuard, RbacGuard) // RbacGuard จะเช็ค permission
export class UserController {
constructor(
private readonly userService: UserService,
private readonly assignmentService: UserAssignmentService, // <--- ✅ Inject Service เข้ามา
private readonly assignmentService: UserAssignmentService,
private readonly preferenceService: UserPreferenceService, // ✅ Inject Service
) {}
// --- User CRUD ---
// --- User Preferences (Me) ---
// ต้องวางไว้ก่อน :id เพื่อไม่ให้ route ชนกัน
@Get('me/preferences')
@ApiOperation({ summary: 'Get my preferences' })
@UseGuards(JwtAuthGuard) // Bypass RBAC check for self
getMyPreferences(@CurrentUser() user: User) {
return this.preferenceService.findByUser(user.user_id);
}
@Patch('me/preferences')
@ApiOperation({ summary: 'Update my preferences' })
@UseGuards(JwtAuthGuard) // Bypass RBAC check for self
updateMyPreferences(
@CurrentUser() user: User,
@Body() dto: UpdatePreferenceDto,
) {
return this.preferenceService.update(user.user_id, dto);
}
@Get('me/permissions')
@ApiOperation({ summary: 'Get my permissions' })
@UseGuards(JwtAuthGuard)
getMyPermissions(@CurrentUser() user: User) {
return this.userService.getUserPermissions(user.user_id);
}
// --- User CRUD (Admin) ---
@Post()
@ApiOperation({ summary: 'Create new user' })
@RequirePermission('user.create')
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@Get()
@ApiOperation({ summary: 'List all users' })
@RequirePermission('user.view')
findAll() {
return this.userService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get user details' })
@RequirePermission('user.view')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.userService.findOne(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update user' })
@RequirePermission('user.edit')
update(
@Param('id', ParseIntPipe) id: number,
@@ -58,6 +100,7 @@ export class UserController {
}
@Delete(':id')
@ApiOperation({ summary: 'Delete user (Soft delete)' })
@RequirePermission('user.delete')
remove(@Param('id', ParseIntPipe) id: number) {
return this.userService.remove(id);
@@ -65,14 +108,10 @@ export class UserController {
// --- Role Assignment ---
@Post('assign-role') // <--- ✅ ต้องมี @ เสมอครับ
@Post('assign-role')
@ApiOperation({ summary: 'Assign role to user' })
@RequirePermission('permission.assign')
assignRole(@Body() dto: AssignRoleDto, @Request() req: any) {
return this.assignmentService.assignRole(dto, req.user);
}
@Get('me/permissions')
@UseGuards(JwtAuthGuard) // No RbacGuard here to avoid circular dependency check issues
getMyPermissions(@Request() req: any) {
return this.userService.getUserPermissions(req.user.user_id);
assignRole(@Body() dto: AssignRoleDto, @CurrentUser() user: User) {
return this.assignmentService.assignRole(dto, user);
}
}
+29 -11
View File
@@ -1,24 +1,42 @@
// File: src/modules/user/user.module.ts
// บันทึกการแก้ไข: รวม UserPreferenceService และ RoleService (T1.3)
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';
import { UserAssignmentService } from './user-assignment.service.js';
import { UserAssignment } from './entities/user-assignment.entity.js';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { UserAssignmentService } from './user-assignment.service';
import { UserPreferenceService } from './user-preference.service'; // ✅ เพิ่ม
// Entities
import { User } from './entities/user.entity';
import { UserAssignment } from './entities/user-assignment.entity';
import { UserPreference } from './entities/user-preference.entity';
import { Role } from './entities/role.entity';
import { Permission } from './entities/permission.entity';
@Module({
imports: [
// 3. ลงทะเบียน Entity ทั้ง User และ UserAssignment
TypeOrmModule.forFeature([User, UserAssignment]),
], // 2. เพิ่มบรรทัดนี้ เพื่อบอก NestJS ว่ามี Controller นี้อยู่
// ลงทะเบียน Entity ให้ครบ
TypeOrmModule.forFeature([
User,
UserAssignment,
UserPreference,
Role,
Permission,
]),
],
controllers: [UserController],
providers: [
UserService,
UserAssignmentService, // <--- 4. ลงทะเบียน Service เป็น Provider
UserAssignmentService,
UserPreferenceService, // ✅ เพิ่ม Provider
],
exports: [
UserService,
UserAssignmentService, // <--- 5. Export เผื่อที่อื่นใช้
], // Export ให้ AuthModule เรียกใช้ได้
UserAssignmentService,
UserPreferenceService, // Export ให้ Module อื่นใช้ (เช่น Notification)
],
})
export class UserModule {}