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)).
ทำงานอย่างไร
- ทุก route อยู่ใต้
r.Use(AdminAuth(adminAPIKey))(router.go:213-215).AdminAuth(middleware/adminauth.go:18) อ่าน headerX-Admin-Api-Keyเทียบกับ envADMIN_API_KEYด้วยsubtle.ConstantTimeCompare; ถ้าตั้งกุญแจเป็นค่าว่าง จะปฏิเสธทุกคำขอ (fail closed) → 401 รูปแบบapperr(รูปแบบ error และ response ของ API (apperr + response helper)). คอมเมนต์ในโค้ดเรียกว่า "stopgap" ที่มาก่อน middleware JWT/permission (adminauth.go:13-17). - รูปแบบตอบของ 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). openPool(ctx, provider, dbName)(handler.go:44): ตัดช่องว่าง provider (ว่าง →INVALID_PROVIDER), หาการเชื่อมต่อจาก registry ผ่านtenants.GetProviderConnection(ทะเบียน Provider แบบไฟล์ JSON (Provider registry)),dbNameว่าง = Main DBsmlerpmain<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), เช็กซ้ำแบบไม่สนตัวพิมพ์ (409DUPLICATE_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); ถ้าผู้ใช้มีอีเมล จะ upsertsml_google_workspace_memberด้วย roleADMIN(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,''),เดิม)จึงแก้เฉพาะที่ส่งมา) และตาม renamedata_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)). ไม่พบรหัสเจ้าของ → 404NOT_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และสิทธิ์ทั้งหมด — ต้องเก็บเป็นความลับ ตั้งผ่าน envADMIN_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)