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