EduCore & FinFlow Workspace ตอนที่ 1: สถาปัตยกรรม Backend V2, สิทธิ์การใช้งานแบบ Wildcard RBAC และกลไกการสืบทอดระบบใน Phase 2

เจาะลึกระบบ Backend ของ EduCore & FinFlow Workspace ออกแบบด้วย Node.js (Express) + MongoDB (Mongoose) พร้อมโครงสร้างตรวจสอบสิทธิ์แบบ Wildcard Role-Based Access Control และการรับช่วงต่อจาก Legacy System ใน Phase 2

· 10 min read

Problem

ระบบหลังบ้านเดิม (Legacy V1) ขาดโครงสร้างเลเยอร์และสถาปัตยกรรมที่ชัดเจน โค้ดทั้งหมดเขียนแบบไม่มี Service/Repository Layer สิทธิ์การใช้งานของผู้ใช้งานผูกเข้ากับโค้ดแบบแข็งตัว (Hard-coded) มีการคิวรีฐานข้อมูลโดยตรงจาก Controller และมีช่องโหว่ความปลอดภัยด้านธุรกรรมงบประมาณ

Solution

ออกแบบและพัฒนา API V2 ใหม่ภายใต้รูปแบบ Clean Layered Architecture ร่วมกับระบบ Dependency Injection ผ่าน Factory Pattern พร้อมนำระบบจำกัดและตรวจสอบสิทธิ์แบบสิทธิ์แบบละเอียด (Granular Wildcard RBAC) และระบบอนุมัติเอกสารแบบเครื่องจักรสถานะ (State Machine)

Impact

ลดเวลาการประมวลผลคำขอ (Response Time) เฉลี่ยเหลือต่ำกว่า 200ms แยกความรับผิดชอบของโค้ดส่งผลให้ทำ Unit Test ได้ถึง 80%+ และช่วยให้องค์กรบริหารงบประมาณได้อย่างโปร่งใสตรวจสอบได้ 100%

1. บทนำและบริบทการรับช่วงต่อโครงการใน Phase 2 (Legacy System Context)

ระบบบริหารจัดการสารสนเทศทางการศึกษาและงบประมาณกลาง EduCore & FinFlow Workspace เป็นระบบระดับสถาบันที่รองรับกระบวนการขออนุมัติจัดซื้อจัดจ้างและงบประมาณ

ในการรับช่วงต่อระบบใน Phase 2 ทีมพัฒนาพบข้อจำกัดสำคัญของระบบ Legacy V1:

  • ขาดการแบ่งเลเยอร์ (Router ➔ Controller ➔ Model): ไม่มี Service/Repository layer ทำให้ Controller คิวรีฐานข้อมูลโดยตรงและจัดการ Business Logic ผสมปนเปกัน ส่งผลให้โค้ดมีความยาวและซับซ้อนสูง
  • Dependency ผูกติดกันแน่น (Tight Coupling): ไม่สามารถเขียน Unit Test ได้อย่างมีประสิทธิภาพ
  • ระบบสิทธิ์แบบแข็งตัว: การตรวจสอบสิทธิ์การใช้งานของแต่ละตำแหน่งงานเป็นไปอย่างจำกัด

ใน Phase 2 นี้ เราจึงปรับปรุงด้วย V2 Architecture (พัฒนาแยกเด็ดขาดในพาธ /v2 กว่า 90% ของฟีเจอร์ใหม่) เพื่อให้ระบบขยายตัวได้ยืดหยุ่นและปลอดภัย


2. เส้นทางการรับส่งข้อมูลและสถาปัตยกรรมเลเยอร์ (V2 Request/Response Flow)

การทำงานของข้อมูลในระบบ V2 ถูกออกแบบให้แยกความรับผิดชอบอย่างชัดเจนตามหลัก Clean Architecture:

sequenceDiagram
    autonumber
    actor Client as Client App
    participant Router as Route Definition (/v2)
    participant Auth as Auth Middleware (JWT)
    participant Authz as Authorize Middleware (RBAC)
    participant Val as Validator Middleware (Zod)
    participant Ctrl as Controller Layer
    participant Svc as Service Layer (Business Rules)
    participant Repo as Repository Layer (DB Access)
    participant DB as MongoDB (Mongoose)

    Client->>Router: HTTP POST /api/v2/fiscal-year (with token)
    Router->>Auth: Verify JWT & Populate req.employee
    Router->>Authz: checkPermissions(resource, action)
    Router->>Val: validateSchema(DTO)
    Router->>Ctrl: Invoke handler method via Factory
    Ctrl->>Svc: Call service.create(validatedData)
    Note over Svc: Validate business rules
    Svc->>Repo: Call repository.create(data)
    Repo->>DB: mongooseModel.create(data)
    DB-->>Client: SuccessResponse(201, data)

การจัดระเบียบ Middleware Chain

ทุก Endpoint V2 จะต้องผ่าน Middleware ตามลำดับ:

  1. auth: ถอดรหัสและตรวจความถูกต้องของ JWT เพื่อระบุตัวตนใน req.employee
  2. authorize: ตรวจสอบความถูกต้องของสิทธิ์การเข้าถึงข้อมูลตามนโยบาย Wildcard RBAC
  3. validator: ตรวจสอบโครงสร้างพารามิเตอร์ขาเข้าผ่าน Zod DTO หากผิดพลาดจะตีกลับทันทีเพื่อประหยัดทรัพยากรฐานข้อมูล

3. โครงสร้างโฟลเดอร์ระบบ V2

โค้ด V2 ทั้งหมดถูกแยกออกจาก V1 และมีระบบตรวจสอบกลางร่วมกันใน /common:

  • common/: ระบบแชร์ร่วมกัน เช่น สิทธิ์การใช้งาน (auth), Custom Error, ข้อมูลภาษา (i18n), Winston logger และ advanceQuery (สำหรับการค้นหา/แบ่งหน้า)
  • router/v2/ & controller/v2/: สำหรับจัดการ Route และรับส่ง HTTP โดยห้ามมี Business Logic
  • services/ & repositories/: บรรจุข้อกำหนดทางธุรกิจ (Business Logic) และคำสั่งติดต่อฐานข้อมูล (Database Access) ตามลำดับ
  • model/v2/: เก็บโครงสร้าง Mongoose Schema และข้อกำหนดตรวจสอบพารามิเตอร์ DTO (Zod)

4. เจาะลึกซอร์สโค้ดตัวอย่างโมดูล V2 (ปีงบประมาณ - Fiscal Year)

เพื่อให้เห็นรูปแบบการทำงานจริง เรามาดูตัวอย่างการจัดทำโครงสร้างโมดูล ปีงบประมาณ (Fiscal Year) ใน V2 Stack:

4.1 Schema และ DTO (Zod Validation)

จัดเก็บไว้ใน model/v2/fiscal-year.model/ เพื่อใช้ตรวจสอบข้อมูลฝั่งขาเข้า:

// fiscal-year.model.js (Mongoose Model)
const mongoose = require("mongoose");
const FiscalYearSchema = new mongoose.Schema(
  {
    year: { type: Number, required: true, unique: true },
    description: { type: String, required: true },
    budgetLimit: { type: Number, required: true },
    created_by: { type: mongoose.Schema.Types.ObjectId, ref: "Employee", required: true },
  },
  { timestamps: true, versionKey: false }
);
module.exports = mongoose.model("FiscalYear", FiscalYearSchema);

// fiscal-year.dto.js (Zod Validation Schemas)
const { z } = require("zod");
const createFiscalYearSchema = z.object({
  year: z.number().int().min(2500, "ระบุปี พ.ศ. ให้ถูกต้อง"),
  description: z.string().min(3),
  budgetLimit: z.number().positive(),
});
module.exports = { createFiscalYearSchema };

4.2 ข้อมูลเลเยอร์และตรรกะระบบ (Repository & Service Layer)

  • Repository: เน้นจัดการกับ Mongoose query โดยตรง ไม่มีส่วนผสมของกฎเกณฑ์ธุรกิจ
  • Service: บรรจุกฎทางธุรกิจ เช่น ป้องกันการสร้างปีงบประมาณซ้ำซ้อน
// fiscal-year.repository.js
class FiscalYearRepository {
  constructor(model) {
    this.model = model;
  }
  async findOne(filter) {
    return this.model.findOne(filter).exec();
  }
  async create(data) {
    return this.model.create(data);
  }
}

// fiscal-year.service.js
const { BadRequestError } = require("../../common/error/error");
class FiscalYearService {
  constructor(fiscalYearRepo) {
    this.fiscalYearRepo = fiscalYearRepo;
  }
  async create(data, actorId) {
    const existing = await this.fiscalYearRepo.findOne({ year: data.year });
    if (existing) throw new BadRequestError(`ปีงบประมาณ ${data.year} ถูกสร้างไว้ในระบบแล้ว`);
    return this.fiscalYearRepo.create({ ...data, created_by: actorId });
  }
}

4.3 ตัวเชื่อมต่อระบบและเส้นทาง (Controller, Factory & Router)

ใช้ Factory Pattern ในการผูกและส่งผ่านโครงสร้าง Dependency Injection เพื่อรักษาระเบียบของชุดคำสั่ง:

// fiscal-year.controller.js
const { asyncHandler } = require("../../../common/middlewares/asyncHandler");
const { SuccessResponse } = require("../../../common/response/response");
class FiscalYearController {
  constructor(service) {
    this.service = service;
  }
  create = asyncHandler(async (req, res) => {
    const result = await this.service.create(req.body, req.employee._id);
    new SuccessResponse("สร้างสำเร็จ", result, 201).send(res);
  });
}

// fiscal-year.factory.js (Dependency Injection Assembly)
const FiscalYearModel = require("../../../model/v2/fiscal-year.model/fiscal-year.model");
const FiscalYearRepository = require("../../../repositories/fiscal-year.repository");
const FiscalYearService = require("../../../services/fiscal-year/fiscal-year.service");
const FiscalYearController = require("../../../controller/v2/fiscal-year/fiscal-year.controller");
module.exports = {
  makeController: () =>
    new FiscalYearController(new FiscalYearService(new FiscalYearRepository(FiscalYearModel))),
};

// index.js (Router Definition)
const router = require("express").Router();
const { makeController } = require("./fiscal-year.factory");
const { auth } = require("../../../authentication/auth");
const { authorize } = require("../../../common/auth/authorization");
const { validator } = require("../../../common/helpers/validator/validator");
const {
  createFiscalYearSchema,
} = require("../../../model/v2/fiscal-year.model/dtos/fiscal-year.dto");

router.post(
  "/",
  auth,
  authorize("budget", "create"),
  validator({ schema: createFiscalYearSchema, source: "body" }),
  makeController().create
);
module.exports = router;

5. กลไกการตรวจสอบสิทธิ์ความปลอดภัยด้วย Wildcard RBAC

ระบบปกป้องข้อมูลผ่านการตรวจสอบระดับทรัพยากร (Resource) และการกระทำ (Action) ในฐานข้อมูล:

  • การจับคู่สิทธิ์แบบละเอียด: ตรวจสอบข้อมูล Roles ในโทเค็นของผู้ใช้อ้างอิงเข้ากับ Resource เช่น budget และ Action เช่น create หรือ read
  • สิทธิ์ครอบคลุมสูงสุด (Wildcard RBAC): ในกรณีที่ผู้ใช้มีสิทธิ์ระดับผู้ดูแลระบบสูงสุด (Superadmin) ระบบจะระบุทรัพยากรเป็นเครื่องหมาย * (Wildcard) ซึ่งมีผลในการข้ามผ่านการตรวจสอบและประเมินผลของ Middleware ทั้งหมดโดยอัตโนมัติ ทำให้ดูแลรักษาสิทธิ์พนักงานได้ง่ายและปลอดภัย