From c414899a4f0dcca7ddf2b7d1a222e1ede433b1fc Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 4 Oct 2025 16:46:39 +0700 Subject: [PATCH] feat(dashboard): backend and frontend --- README.md | 2 +- backend/src/middleware/index.js | 39 +++ .../admin/_components/user-form-dialog.jsx | 315 ++++++++++++------ 3 files changed, 245 insertions(+), 111 deletions(-) create mode 100644 backend/src/middleware/index.js diff --git a/README.md b/README.md index 49a18501..d20c6234 100755 --- a/README.md +++ b/README.md @@ -132,5 +132,5 @@ git checkout -b feature/dashboard-update-251004 # แก้ไฟล์ frontend/app/dashboard/\* และที่เกี่ยวข้อง git add frontend/app/dashboard -git commit -m "feat(dashboard): เพิ่ม KPI tiles + แก้ layout grid" +git commit -m "feat(dashboard): เพิ่มส่วนจัดการ user" git push -u origin feature/dashboard-update-251004 diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js new file mode 100644 index 00000000..27ff72ba --- /dev/null +++ b/backend/src/middleware/index.js @@ -0,0 +1,39 @@ +// File: backend/src/middleware/index.js +import * as abac from "./abac.js"; +import * as auth from "./auth.js"; +import * as authJwt from "./authJwt.js"; +import * as errorHandler from "./errorHandler.js"; +import * as loadPrincipal from "./loadPrincipal.js"; +import * as permGuard from "./permGuard.js"; +import * as permissions from "./permissions.js"; +import * as rbac from "./rbac.js"; +import * as requirePerm from "./requirePerm.js"; + +// Export ทุกอย่างออกมาเป็น named exports +// เพื่อให้สามารถ import แบบ `import { authJwt, permGuard } from '../middleware';` ได้ +export { + abac, + auth, + authJwt, + errorHandler, + loadPrincipal, + permGuard, + permissions, + rbac, + requirePerm, +}; + +// (Optional) สร้าง default export สำหรับกรณีที่ต้องการ import ทั้งหมดใน object เดียว +const middleware = { + abac, + auth, + authJwt, + errorHandler, + loadPrincipal, + permGuard, + permissions, + rbac, + requirePerm, +}; + +export default middleware; diff --git a/frontend/app/(protected)/admin/_components/user-form-dialog.jsx b/frontend/app/(protected)/admin/_components/user-form-dialog.jsx index b48c1010..08c6f751 100644 --- a/frontend/app/(protected)/admin/_components/user-form-dialog.jsx +++ b/frontend/app/(protected)/admin/_components/user-form-dialog.jsx @@ -4,90 +4,143 @@ import { useState, useEffect } from 'react'; import api from '@/lib/api'; import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, -} from '@/components/ui/dialog'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; -import { Checkbox } from "@/components/ui/checkbox" +import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Trash2 } from 'lucide-react'; +import { ScrollArea } from '@/components/ui/scroll-area'; + export function UserFormDialog({ user, isOpen, setIsOpen, onSuccess }) { const [formData, setFormData] = useState({}); const [allRoles, setAllRoles] = useState([]); - const [selectedRoles, setSelectedRoles] = useState(new Set()); + const [selectedSystemRoles, setSelectedSystemRoles] = useState(new Set()); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); + const [allProjects, setAllProjects] = useState([]); + const [projectRoles, setProjectRoles] = useState([]); + const [selectedProjectId, setSelectedProjectId] = useState(''); + const [selectedRoleId, setSelectedRoleId] = useState(''); const isEditMode = !!user; useEffect(() => { - // ดึงข้อมูล Role ทั้งหมดมาเตรียมไว้ - const fetchRoles = async () => { + const fetchPrerequisites = async () => { try { - const res = await api.get('/rbac/roles'); - setAllRoles(res.data); + const [rolesRes, projectsRes] = await Promise.all([ + api.get('/rbac/roles'), + api.get('/projects'), + ]); + setAllRoles(rolesRes.data); + setAllProjects(projectsRes.data); } catch (err) { - console.error('Failed to fetch roles', err); + console.error('Failed to fetch prerequisites', err); } }; - fetchRoles(); - }, []); + if (isOpen) { + fetchPrerequisites(); + } + }, [isOpen]); useEffect(() => { - // เมื่อ user prop เปลี่ยน (เปิด dialog เพื่อแก้ไข) ให้ตั้งค่าฟอร์ม - if (isEditMode) { - setFormData({ - username: user.username, - email: user.email, - first_name: user.first_name || '', - last_name: user.last_name || '', - is_active: user.is_active, - }); - setSelectedRoles(new Set(user.Roles?.map(role => role.id) || [])); - } else { - // ถ้าเป็นการสร้างใหม่ ให้เคลียร์ฟอร์ม - setFormData({ - username: '', - email: '', - password: '', - first_name: '', - last_name: '', - is_active: true, - }); - setSelectedRoles(new Set()); + const fetchUserData = async () => { + if (isEditMode) { + setFormData({ + username: user.username, + email: user.email, + first_name: user.first_name || '', + last_name: user.last_name || '', + is_active: user.is_active, + }); + setSelectedSystemRoles(new Set(user.Roles?.map(role => role.id) || [])); + + try { + const res = await api.get(`/rbac/user-project-roles?userId=${user.id}`); + setProjectRoles(res.data); + } catch (err) { + console.error("Failed to fetch user's project roles", err); + setProjectRoles([]); + } + + } else { + setFormData({ username: '', email: '', password: '', first_name: '', last_name: '', is_active: true }); + setSelectedSystemRoles(new Set()); + setProjectRoles([]); + } + setError(''); + setSelectedProjectId(''); + setSelectedRoleId(''); + }; + + if (isOpen) { + fetchUserData(); } - setError(''); }, [user, isOpen]); const handleInputChange = (e) => { const { id, value } = e.target; setFormData((prev) => ({ ...prev, [id]: value })); }; - - const handleRoleChange = (roleId) => { - setSelectedRoles(prev => { + + const handleSystemRoleChange = (roleId) => { + setSelectedSystemRoles(prev => { const newSet = new Set(prev); - if (newSet.has(roleId)) { - newSet.delete(roleId); - } else { - newSet.add(roleId); - } + if (newSet.has(roleId)) newSet.delete(roleId); + else newSet.add(roleId); return newSet; }); }; - const handleSubmit = async (e) => { - e.preventDefault(); + const handleAddProjectRole = async () => { + if (!selectedProjectId || !selectedRoleId) { + setError("Please select both a project and a role."); + return; + } + setIsLoading(true); + setError(''); + try { + await api.post('/rbac/user-project-roles', { + userId: user.id, + projectId: selectedProjectId, + roleId: selectedRoleId + }); + const res = await api.get(`/rbac/user-project-roles?userId=${user.id}`); + setProjectRoles(res.data); + setSelectedProjectId(''); + setSelectedRoleId(''); + } catch(err) { + setError(err.response?.data?.message || 'Failed to add project role.'); + } finally { + setIsLoading(false); + } + }; + + const handleRemoveProjectRole = async (assignment) => { + setIsLoading(true); + setError(''); + try { + await api.delete('/rbac/user-project-roles', { + data: { + userId: user.id, + projectId: assignment.project_id, + roleId: assignment.role_id + } + }); + setProjectRoles(prev => prev.filter(p => p.id !== assignment.id)); + } catch(err) { + setError(err.response?.data?.message || 'Failed to remove project role.'); + } finally { + setIsLoading(false); + } + }; + + const handleSaveUserDetails = async () => { setIsLoading(true); setError(''); - - const payload = { ...formData, roles: Array.from(selectedRoles) }; + const payload = { ...formData, roles: Array.from(selectedSystemRoles) }; try { if (isEditMode) { @@ -95,77 +148,119 @@ export function UserFormDialog({ user, isOpen, setIsOpen, onSuccess }) { } else { await api.post('/users', payload); } - onSuccess(); // บอกให้หน้าหลัก refresh ข้อมูล - setIsOpen(false); // ปิด Dialog + onSuccess(); + setIsOpen(false); } catch (err) { setError(err.response?.data?.message || 'An unexpected error occurred.'); } finally { setIsLoading(false); } }; - + return ( - -
- - {isEditMode ? 'Edit User' : 'Create New User'} - - {isEditMode ? `Editing ${user.username}` : 'Fill in the details for the new user.'} - - -
-
- - + + + {isEditMode ? `Edit User: ${user.username}` : 'Create New User'} + + +
+ + {/* Section 1: User Details & System Roles */} +
+

User Details & System Roles

+
+ + +
+
+ + +
+ {!isEditMode && ( +
+ + +
+ )} +
+
+ + +
+
+ + +
+
+
+ +
+ {allRoles.map(role => ( +
+ handleSystemRoleChange(role.id)} /> + +
+ ))} +
+
+
+ setFormData(prev => ({...prev, is_active: checked}))} /> + +
-
- - -
- {!isEditMode && ( -
- - -
- )} -
- - -
-
- - -
-
- -
- {allRoles.map(role => ( -
- handleRoleChange(role.id)} - /> - + + {/* Section 2: Project Role Assignments */} +
+

Project Role Assignments

+ {isEditMode ? ( + <> +
+

Assign New Project Role

+
+ + +
+
- ))} -
+ +
+

Current Assignments

+
+ {projectRoles.length > 0 ? projectRoles.map(pr => ( +
+
+ {pr.Project.name} + as + {pr.Role.name} +
+ +
+ )) :

No project assignments.

} +
+
+ + ) :

Save the user first to assign project roles.

}
-
- - setFormData(prev => ({...prev, is_active: checked}))} /> -
-
- {error &&

{error}

} - -
+ + {error &&

{error}

} + + + - - +
);