160 lines
6.4 KiB
Markdown
160 lines
6.4 KiB
Markdown
# 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 ข้างบน
|