Files
lcbp3.np-dms.work/backend/README2.md

160 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 ข้างบน