05.1 ปรบปรง backend ทงหมด และ frontend/login
This commit is contained in:
159
backend/README2.md
Normal file
159
backend/README2.md
Normal 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 ข้างบน
|
||||
Reference in New Issue
Block a user