05.1 ปรบปรง backend ทงหมด และ frontend/login

This commit is contained in:
admin
2025-10-01 11:14:11 +07:00
parent 5be0f5407b
commit 905afb56f5
43 changed files with 2285 additions and 2834 deletions

159
backend/README2.md Normal file
View File

@@ -0,0 +1,159 @@
# DMS Backend คู่มือ Auth & RBAC/ABAC
## ภาพรวมระบบ
Backend ใช้ **Bearer Token** สำหรับการยืนยันตัวตน และตรวจสอบสิทธิ์ด้วย **RBAC (Role-Based Access Control)** ร่วมกับ **ABAC (Attribute-Based Access Control)**
โครงหลักคือ:
1. **authJwt()** → ตรวจสอบ JWT ใน header `Authorization: Bearer ...`
2. **loadPrincipalMw()** → โหลดข้อมูลผู้ใช้ + บทบาท + สิทธิ์ + ขอบเขตโปรเจ็ค/องค์กร
3. **requirePerm()** → ตรวจสอบ `perm_code` จากตาราง `permissions` และบังคับ ABAC (ORG/PROJECT scope)
---
## การยืนยันตัวตน (Authentication)
### Frontend ส่งอย่างไร
```http
GET /api/projects
Authorization: Bearer <access_token>
```
- **ไม่มีการใช้ cookie** (Bearer-only)
- ถ้า token หมดอายุ ให้ใช้ `refresh_token` ไปขอใหม่ที่ `/api/auth/refresh`
### Middleware `authJwt()`
- อ่าน `Authorization: Bearer ...`
- ตรวจสอบด้วย `JWT_SECRET`
- เติม `req.auth = { user_id, username }`
---
## การโหลด Principal
### Middleware `loadPrincipalMw()`
- ใช้ `user_id` ไป query DB:
- users, roles, permissions, project_ids, org_ids
- สร้าง `req.principal`:
```js
{
user_id, username, email, first_name, last_name, org_id,
roles: [{ role_id, role_code, role_name }],
permissions: Set<perm_code>,
project_ids: [..],
org_ids: [..],
is_superadmin: true/false,
// helper functions
can(code),
canAny(codes[]),
canAll(codes[]),
inProject(pid),
inOrg(oid)
}
```
---
## การตรวจสอบสิทธิ์ (RBAC + ABAC)
### Middleware `requirePerm(permCode, { projectParam?, orgParam? })`
1. ตรวจว่า user มี `permCode` หรือเป็น superadmin
2. อ่าน `scope_level` จากตาราง `permissions`
- `GLOBAL` → มีสิทธิ์ก็พอ
- `ORG` → ต้องมีสิทธิ์ + อยู่ใน org scope
- `PROJECT` → ต้องมีสิทธิ์ + อยู่ใน project scope
3. อ่าน `project_id` / `org_id` จาก request (`params`, `query`, `body`)
4. ถ้าไม่ผ่าน → คืน `403 FORBIDDEN`
### Error response ตัวอย่าง
```json
{ "error": "FORBIDDEN", "need": "projects.manage" }
{ "error": "FORBIDDEN_PROJECT", "project_id": 12 }
{ "error": "FORBIDDEN_ORG", "org_id": 5 }
```
---
## รูปแบบที่แนะนำ
### List (PROJECT scope)
```js
r.get("/", requirePerm("documents.view", { projectParam: "project_id" }), async (req, res) => {
const P = req.principal;
const { project_id } = req.query;
const cond = [], params = [];
if (!P.is_superadmin) {
if (project_id) {
if (!P.inProject(+project_id)) return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
cond.push("project_id=?"); params.push(+project_id);
} else if (P.project_ids?.length) {
cond.push(`project_id IN (${P.project_ids.map(()=>"?").join(",")})`);
params.push(...P.project_ids);
}
}
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
const [rows] = await sql.query(`SELECT * FROM documents ${where} ORDER BY created_at DESC LIMIT 50`, params);
res.json(rows);
});
```
### Item (PROJECT scope)
```js
r.delete("/:id", requirePerm("drawings.delete"), async (req, res) => {
const id = +req.params.id;
const [[row]] = await sql.query("SELECT project_id FROM drawings WHERE id=?", [id]);
if (!row) return res.status(404).json({ error: "Not found" });
if (!req.principal.is_superadmin && !req.principal.inProject(row.project_id)) {
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
}
await sql.query("DELETE FROM drawings WHERE id=?", [id]);
res.json({ ok: 1 });
});
```
---
## การแมปสิทธิ์ (perm_code)
| หมวด | สิทธิ์ (perm_code) | scope |
|--------------|---------------------------------------------|------------|
| Organizations| `organizations.view`, `organizations.manage`| GLOBAL |
| Projects | `projects.view`, `projects.manage` | ORG |
| Drawings | `drawings.view`, `drawings.upload`, `drawings.delete` | PROJECT |
| Documents | `documents.view`, `documents.manage` | PROJECT |
| RFAs | `rfas.view`, `rfas.create`, `rfas.respond`, `rfas.delete` | PROJECT |
| Correspondences | `corr.view`, `corr.manage` | PROJECT |
| Transmittals | `transmittals.manage` | PROJECT |
| Reports | `reports.view` | GLOBAL |
| Settings | `settings.manage` | GLOBAL |
| Admin | `admin.access` | ORG |
---
## Checklist สำหรับเพิ่ม Endpoint ใหม่
1. เลือก `perm_code` ที่ตรงกับ seed
2. ใส่ `requirePerm("<perm>", { projectParam?: "...", orgParam?: "..." })`
3. ถ้าเป็น GET/PUT/DELETE record เดี่ยว → ตรวจสอบซ้ำด้วย `inProject`/`inOrg`
4. ใช้ `callProc("sp_name", [...])` ถ้า endpoint เรียก Stored Procedure
5. ฝั่ง FE ต้องส่ง `Authorization: Bearer ...` และ parameter `project_id`/`org_id` ที่จำเป็น
---
## Pitfalls ที่พบบ่อย
- ลืมส่ง `project_id` ในคำขอ → 403
- อ้าง perm_code ผิด (เช่น `document.view` แทน `documents.view`)
- ไม่กรอง project/org scope ใน query → ข้อมูลรั่ว
- ลืมเช็ค item-level ABAC → ข้ามขอบเขตได้
- ปน cookie-auth เข้ามา → backend จะไม่รองรับแล้ว
---
## TL;DR
- ทุกคำขอ → Bearer Token
- `authJwt()` → ใส่ `req.auth`
- `loadPrincipalMw()` → ใส่ `req.principal` (roles, perms, scope)
- `requirePerm()` → บังคับ RBAC + ABAC อัตโนมัติ
- เพิ่ม endpoint ใหม่ → ใช้ checklist ข้างบน