251123:2300 Update T1
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user