สมอง SML

คลังความรู้ / หลังบ้าน Go

#backend#admin#http#tenant#security

Admin API (/api/v1/admin/*)

กลุ่ม route สำหรับหน้าจอผู้ดูแลระบบ: จัดการ Provider, ผู้ใช้, รายการบริษัท (sml_database_list), ข้อมูลสรุปฐาน, SQL ตรงกับ Main DB, สิทธิ์เมนู และ Workspace Google (Workspace Google: ล็อกอินด้วยอีเมล สร้างบริษัทใหม่ สมาชิก และผูก LINE (Workspace Google API)). ป้องกันด้วยกุญแจคงที่ตัวเดียว ไม่ใช่ JWT ของผู้ใช้ (JWT session token และการเพิกถอน (JWT + revocation store)). ไม่ได้ทำผ่าน use case ใน application/ ทั้งหมด — handler คุย SQL กับ Main DB ของ Provider เองเป็นส่วนใหญ่ (สถาปัตยกรรม Hexagonal และ composition root ของ Go backend (Hexagonal Architecture + DI)).

ทำงานอย่างไร

  1. ทุก route อยู่ใต้ r.Use(AdminAuth(adminAPIKey)) (router.go:213-215). AdminAuth (middleware/adminauth.go:18) อ่าน header X-Admin-Api-Key เทียบกับ env ADMIN_API_KEY ด้วย subtle.ConstantTimeCompare; ถ้าตั้งกุญแจเป็นค่าว่าง จะปฏิเสธทุกคำขอ (fail closed) → 401 รูปแบบ apperr (รูปแบบ error และ response ของ API (apperr + response helper)). คอมเมนต์ในโค้ดเรียกว่า "stopgap" ที่มาก่อน middleware JWT/permission (adminauth.go:13-17).
  2. รูปแบบตอบของ handler ในแพ็กเกจ admin ต่างจาก API อื่น: สำเร็จ {"ok":true,"data":...}, ล้มเหลว {"ok":false,"error":"...","code":"..."} (handler.go:69-85). code เช่น BAD_JSON, INVALID_PROVIDER (400), DB_CONNECT_ERROR (500), INSERT_USER_ERROR (409).
  3. openPool(ctx, provider, dbName) (handler.go:44): ตัดช่องว่าง provider (ว่าง → INVALID_PROVIDER), หาการเชื่อมต่อจาก registry ผ่าน tenants.GetProviderConnection (ทะเบียน Provider แบบไฟล์ JSON (Provider registry)), dbName ว่าง = Main DB smlerpmain<code> (ตัวพิมพ์เล็ก), ประกอบ DSN แล้วขอ pool จาก PoolManager (ตัวจัดการ connection pool ต่อฐานข้อมูล (PoolManager)). Provider ถูกส่งมาใน body/query ของทุกคำขอ (provider) ไม่ได้มาจาก token.

ตาราง route (router.go:216-250)

กลุ่ม Method + path ทำอะไร
Provider GET/POST/PATCH/DELETE /providers ดู/สร้าง Provider (PATCH, DELETE เป็น stub)
ผู้ใช้ GET/POST/PATCH/DELETE /users จัดการ sml_user_list + สิทธิ์เข้าบริษัท
บริษัท GET/POST/PATCH/DELETE /databases จัดการ sml_database_list
ข้อมูลฐาน GET /database-info นับผู้ใช้/โปรไฟล์บริษัท/option ต่อฐาน
SQL POST /main-query รัน SELECT หรือ DML กับ Main DB/ฐานที่ระบุ
สิทธิ์เมนู GET/PUT /menu-permissions อ่าน/เขียน image_file (และ tooltip)
Workspace /workspace/* (11 route) ดู Workspace Google: ล็อกอินด้วยอีเมล สร้างบริษัทใหม่ สมาชิก และผูก LINE (Workspace Google API)

นอกจากนี้มี /admin/providers (Create/List ผ่าน tenantHandler, router.go:114-119) ที่ใช้ AdminAuth ตัวเดียวกัน — เป็นเส้นทางสร้าง Provider แบบถูกต้องผ่าน use case (ทะเบียน Provider แบบไฟล์ JSON (Provider registry)).

รายละเอียดแต่ละกลุ่ม

Provider — CreateProvider (handler.go:113): รับ provider_code, provider_name, auth_database, is_active; ทำรหัสเป็นพิมพ์เล็ก; auth_database ว่าง = smlerpmain<code>; พยายามเชื่อมฐาน postgres ผ่าน Provider ชื่อ demo เพื่อ CREATE DATABASE ถ้ายังไม่มี (ผลถูกกลืน _ =); จากนั้นเรียก tenants.CreateProvider โดย ข้อมูลเชื่อมต่อ (host/port/user/sslmode) ฝังตายตัวในโค้ด และรหัสผ่านว่าง (:141-148) — ไม่รับจาก body. ซ้ำ/ผิดกติกา → 409 CREATE_PROVIDER_ERROR. UpdateProvider (:165) แค่สะท้อน body กลับ และ DeleteProvider (:172) แค่ตอบ {"deleted":code} — ไม่ได้ทำอะไรกับ registry จริง.

ผู้ใช้ (sml_user_list):

  • ListUsers (:178) รัน ALTER TABLE ... ADD COLUMN IF NOT EXISTS email ทุกครั้ง แล้ว SELECT พร้อมรวมรายชื่อฐานที่ได้สิทธิ์จาก sml_database_list_user_and_group (เฉพาะ user_or_group_status = 0); is_active = active_status == 1.
  • CreateUser (:236): user_code ยาวสุด 50 (USER_CODE_TOO_LONG), เช็กซ้ำแบบไม่สนตัวพิมพ์ (409 DUPLICATE_USER), ขยายคอลัมน์ user_password เป็น VARCHAR(255), ถ้ามีรหัสผ่านจะ hash ด้วย domainidentity.HashPassword (bcrypt) ก่อนเก็บ (ขั้นตอน login / เลือกบริษัท / refresh / logout (Identity login flow)); user_level ไม่ถูกจำกัดช่วง; แล้วเรียก syncUserAllowedDatabases.
  • UpdateUser (:310) รับ field แบบ pointer (ส่งเฉพาะที่แก้) และรับ allowed_databases ซ้ำได้.
  • syncUserAllowedDatabases (:376): ลบแถวสิทธิ์เข้าบริษัทแบบ user ตรง (user_or_group_status = 0) ของผู้ใช้นั้นทั้งหมด แล้ว insert ใหม่ตามรายการ (จับคู่ด้วย data_database_name หรือ data_code); ถ้าผู้ใช้มีอีเมล จะ upsert sml_google_workspace_member ด้วย role ADMIN (level ≥ 9) หรือ MEMBER ด้วย.
  • DeleteUser (:414): ลบจาก sml_database_list_user_and_group, sml_user_and_group, sml_user_list ตามลำดับ (ผล error ถูกกลืน; ไม่อยู่ใน transaction); ไม่ลบแถวสิทธิ์เมนู sml_permissions_user.

บริษัท (sml_database_list):

  • ListDatabases (:429) คืน data_code/data_name/data_database_name/data_group เรียงตาม roworder.
  • CreateDatabase (:467): ชื่อฐานต้องตรง ^[a-z0-9_]{1,63}$ (validDBNameRe, :27) และ ห้ามขึ้นต้น smlerpmain; data_group ว่าง = MAIN; แค่ INSERT แถวลงรายการ — ไม่ได้สร้างฐาน PostgreSQL จริง; แล้วให้สิทธิ์เข้าบริษัทอัตโนมัติแก่ผู้ใช้ SUPERADMIN, admin, 0001 (:502-508).
  • UpdateDatabase (:517) ใน transaction: แก้ data_code/data_name/data_group (ใช้ COALESCE(NULLIF(x,''),เดิม) จึงแก้เฉพาะที่ส่งมา) และตาม rename data_code/data_group ในตารางสิทธิ์.
  • DeleteDatabase (:580) ลบ เฉพาะแถวในรายการ (ไม่ drop ฐาน, ไม่ลบแถวสิทธิ์ใน sml_database_list_user_and_group).

database-info (:593): วนทุกบริษัทแล้วเปิด pool ของแต่ละฐานเพื่อนับ erp_user, erp_company_profile, erp_option → employee_count, has_company_profile, option_count. option_count เป็นค่าตายตัว 42 เมื่อ erp_option มีข้อมูล ไม่ใช่จำนวนจริง (:631-635); ฐานที่เปิดไม่ได้จะได้ค่าศูนย์เงียบ ๆ. คำขอเดียวเปิด pool ถึง N ฐาน (ดูข้อควรระวัง).

main-query (:646): body {provider, database, sql, mode}.

  • ฐานเป้าหมาย: database ถ้าส่งมา; ถ้าไม่ส่งแต่ SQL มีคำว่า sml_tooltip จะหาฐานบริษัทด้วย resolveCompanyDB; ไม่งั้น Main DB.
  • mode = exec → domainquery.NewWriteStatement (ตรวจ DML เดียว) แล้ว pool.Exec คืน rowsAffected. ไม่ผ่าน ValidateUserLevel/ValidateClientPolicy (นโยบายล็อกการเขียนผ่าน Query Gateway (Query write policy / lockdown)) — ป้องกันด้วย ADMIN_API_KEY อย่างเดียว ดังนั้นตารางเงินเขียนผ่านทางนี้ได้.
  • โหมดอื่น → NewStatement (SELECT/WITH เท่านั้น) ใน tx ReadOnly+RepeatableRead แบบ SimpleProtocol; ค่า NUMERIC ถูกแปลงเป็นสตริงด้วย NormalizeValue (Query Gateway: /query/list, limit, batch, exec, execBatch). ต่างจาก gateway หลักตรงที่ ไม่มีเพดาน 50,000 แถว และไม่มี timeout 30 วินาที ใน handler นี้ (ไม่พบในโค้ด handler.go:646-722).

menu-permissions (menu_permissions.go):

  • kind=user|group (menuPermissionTables, :18-21) → ตาราง sml_permissions_user|group, ต้องมีรหัสอยู่ใน sml_user_list|sml_group_list; รหัสเมนู/รหัสเจ้าของยาวสุด 100.
  • GET คืน image_file ล่าสุดของ (usercode, database_name) ถอดรหัสด้วย DecodeMenuRights; PUT (body จำกัด 1MB) ลบแถวเดิมแล้ว INSERT แถวใหม่ใน transaction ด้วย EncodeMenuRights เป็น JSON (:107-212) (สิทธิ์เมนูและ middleware RequireRight (Menu rights / permission)). ไม่พบรหัสเจ้าของ → 404 NOT_FOUND.
  • kind=tooltip ใช้ตาราง sml_tooltip(code, name_1, name_2) (สร้างด้วย CREATE TABLE IF NOT EXISTS ทุกครั้งที่เรียก, :58,139); GET คืนไม่เกิน 200 แถว.
  • ensureMenuPermissionColumns (:25) เติมคอลัมน์ database_name ให้ตารางสิทธิ์ทุกคำขอ — แบบเดียวกับฝั่งอ่านสิทธิ์.

ข้อควรระวัง

  • กุญแจเดียว (ADMIN_API_KEY) เปิดได้ทั้งการสร้างผู้ใช้ระดับ 9, อ่าน/เขียนข้อมูลบัญชีผ่าน main-query และสิทธิ์ทั้งหมด — ต้องเก็บเป็นความลับ ตั้งผ่าน env ADMIN_API_KEY เท่านั้น (ตัวแปรสภาพแวดล้อมและการโหลด config ของ backend (Environment config)).
  • ผลของ SQL ส่วนใหญ่ใน handler นี้ถูกกลืน (_, _ =) และไม่อยู่ใน transaction (ยกเว้น UpdateDatabase, SaveMenuPermissions) → สำเร็จบางส่วนแต่ตอบ ok:true ได้ (เช่น DeleteUser, CreateDatabase ส่วน grant).
  • ปุ่มลบ/แก้ Provider และลบฐานในหน้าจอไม่ทำสิ่งที่ชื่อบอกครบ (stub / ลบแค่แถวรายการ) — ตรวจก่อนอธิบายให้ผู้ใช้.
  • ListUsers/CreateUser/UpdateUser รัน ALTER TABLE ทุกครั้ง; database-info เปิด pool หลายฐานในคำขอเดียวและ pool ไม่ถูกปล่อยคืน (ตัวจัดการ connection pool ต่อฐานข้อมูล (PoolManager)).
  • CONTEXT.md/docs/PLAN.md อธิบาย admin เป็นแค่ /admin/providers; โค้ดปัจจุบันมีชุด /api/v1/admin/* เต็ม — ให้ยึดโค้ด.

ที่มาในโค้ด

  • backend/internal/adapter/inbound/http/router.go:213-250 — route และ AdminAuth ของกลุ่ม; :114-119 /admin/providers (use case).
  • backend/internal/adapter/inbound/http/middleware/adminauth.go:13-31 — header, fail-closed, constant-time compare.
  • backend/internal/adapter/inbound/http/admin/handler.go:22-85 — validDBNameRe, Handler, openPool, writeOK/writeFail.
  • backend/internal/adapter/inbound/http/admin/handler.go:88-176 — provider handler (stub ที่ :165,172).
  • backend/internal/adapter/inbound/http/admin/handler.go:178-427 — ผู้ใช้ + syncUserAllowedDatabases.
  • backend/internal/adapter/inbound/http/admin/handler.go:429-644 — บริษัท + DatabaseInfo.
  • backend/internal/adapter/inbound/http/admin/handler.go:646-722 — MainQuery.
  • backend/internal/adapter/inbound/http/admin/menu_permissions.go:18-216 — สิทธิ์เมนู/tooltip.

ดูเพิ่ม

ภาพรวม Backend Go ของ SML Ai Account (Backend overview) · Router, middleware และแผนผัง route ทั้งหมดของ backend (chi router) · ทะเบียน Provider แบบไฟล์ JSON (Provider registry) · สิทธิ์เมนูและ middleware RequireRight (Menu rights / permission) · Workspace Google: ล็อกอินด้วยอีเมล สร้างบริษัทใหม่ สมาชิก และผูก LINE (Workspace Google API) · โมเดลผู้เช่าหลายราย: Tenant, Provider, Main DB, Company (Multi-tenant model) · สารบัญ ADR ของ backend (Architecture Decision Records)