สมอง SML

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

#backend#admin#workspace#google#line#multi-tenant

Workspace Google (/api/v1/admin/workspace/*)

ชุด 11 route ที่ให้ผู้ใช้ "ล็อกอินด้วยอีเมล Google" แล้วเลือก/สร้างบริษัท (ฐานข้อมูล) ของตนเอง, เชิญสมาชิกด้วยอีเมล, ขอดูสิทธิ์หน้าจอ และผูกบัญชี LINE. อยู่ใต้ AdminAuth (กุญแจ X-Admin-Api-Key) เหมือน Admin API ส่วนอื่น (Admin API สำหรับจัดการ Provider/ผู้ใช้/ฐานข้อมูล/สิทธิ์เมนู (Admin API)) — backend ไม่ได้ตรวจ Google ID token เอง; ตัวตน (email, google_sub, actor_*) มาจาก body/query ที่ผู้เรียก (หน้าเว็บ/BFF ที่ถือกุญแจ) ส่งมา.

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

  1. ตารางที่ใช้ (สร้างด้วย ensureWorkspaceTables ทุกครั้งที่ handler เรียก, workspace_google.go:36-76; ผลถูกกลืน): sml_google_account (PK email, google_sub, display_name, picture_url, line_user_id, line_display_name, line_linked_at), sml_google_workspace_db (PK database_name, data_code, company_name, owner_email, provider_code), sml_google_workspace_member (PK database_name+email, role, mapped_user_code, invited_by_email) และเติมคอลัมน์ email, line_user_id ให้ sml_user_list. อยู่ใน Main DB ของ Provider (โมเดลผู้เช่าหลายราย: Tenant, Provider, Main DB, Company (Multi-tenant model)); Provider ว่าง = smldemo (ค่า default ในทุก handler).
  2. POST /workspace/google-login (:263): ต้องมี email ที่มี @; upsert sml_google_account (เก็บ google_sub/ชื่อ/รูปเฉพาะค่าที่ไม่ว่าง) แล้วคืนรายการบริษัทที่เข้าถึงได้.
  3. queryAccessibleWorkspaceDatabases (:91) รวมบริษัทจาก 3 แหล่ง แล้ว merge ตามชื่อฐาน (role ADMIN ชนะ), ข้ามชื่อขึ้นต้น smlerpmain:
    • (1) sml_google_workspace_member ตามอีเมล — role MEMBER ต้องผูก sml_user_list ที่ active (ตาม mapped_user_code หรือหาจาก email) ไม่งั้นถูกข้าม.
    • (2) sml_user_list.email ตรงอีเมล + มีสิทธิ์เข้าบริษัทใน sml_database_list_user_and_group (user_or_group_status=0); role = ADMIN ถ้า user_level >= 9.
    • (3) ถ้าส่ง user_code: บริษัทที่ผู้ใช้ได้สิทธิ์ตรง; role ADMIN ถ้า level ≥ 9 หรือรหัส SUPERADMIN/admin; บริษัทที่ Google-owner เป็นคนอื่นจะไม่รั่วให้ เว้นแต่ผู้ใช้ถูกเชิญ/ผูกอยู่ในตารางสมาชิก.
  4. GET /workspace/databases ใช้ฟังก์ชันเดียวกัน (query email, user_code); GET /workspace/check-db-name?name= เรียก checkDatabaseNameAvailable (:343).
  5. กติกาชื่อฐานข้อมูล (:19,345-373): ^[a-z][a-z0-9_]{2,40}$ (ตัวพิมพ์เล็ก) , ห้ามอยู่ใน reservedDBs (:20-33 เช่น postgres, template0/1, smldemo, default, admin, system, public), ห้ามขึ้นต้น smlerpmain หรือ pg_, ห้ามซ้ำกับ sml_database_list/sml_google_workspace_db (ชื่อหรือ data_code) และห้ามมีอยู่แล้วใน pg_database ของเซิร์ฟเวอร์ → ไม่ผ่าน = 409 DB_NAME_TAKEN.
  6. POST /workspace/databases (:533) สร้างบริษัทใหม่ 3 ขั้น:
    • ต้องมี owner_email (หรือ email; ถ้าไม่มีแต่มี user_code จะใช้ user_code พิมพ์เล็กแทน) ไม่งั้น 400 MISSING_OWNER; ชื่อบริษัทว่าง = กิจการ <ชื่อฐานพิมพ์ใหญ่>; data_code ว่าง = ชื่อฐานพิมพ์ใหญ่.
    • CREATE DATABASE ... ENCODING 'UTF8' บนฐาน postgres ของ Provider (:597) แล้ว cloneEmptySchemaFromTemplate ภายใต้ timeout 45 วินาที (:604).
    • ลงทะเบียนใน Main DB: แถว sml_database_list (group MAIN), สิทธิ์เข้าบริษัทให้ SUPERADMIN, admin และ user_code ที่ส่งมา (ถ้าไม่ใช่อีเมล), แถว sml_google_workspace_db และสมาชิก ADMIN (mapped_user_code = user_code หรือ SUPERADMIN ถ้าว่าง/เป็นอีเมล). ผลของการลงทะเบียนถูกกลืน (_, _ =).
  7. cloneEmptySchemaFromTemplate (:395-531): อ่านโครงตารางทั้งหมดใน schema public ของฐานแม่แบบ smldemo จาก catalog (pg_attribute, pg_constraint) แล้ว CREATE TABLE IF NOT EXISTS ทีละตารางในฐานใหม่ (ถ้าพังใช้ DDL แบบพื้นฐานแทน), คัดลอก PRIMARY KEY/UNIQUE, แล้ว seed เฉพาะ erp_option (ทศนิยม 2 ตำแหน่ง), erp_company_profile (ชื่อบริษัท สาขา 00000 "สำนักงานใหญ่") และผู้ใช้ erp_user SUPERADMIN (level 9) + user_code ของผู้สร้าง — ไม่คัดลอกข้อมูลและไม่คัดลอก index/FK/view/function (คัดเฉพาะ PK/UNIQUE; ส่วนอื่นยังไม่ยืนยันว่ามีที่อื่นสร้างให้หรือไม่).
  8. POST /workspace/select-db (:674): รับ database_name|database_code|database + email/user_code/is_demo; หา chosen จากรายการที่เข้าถึงได้ (หรือ is_demo && smldemo ข้ามการตรวจ, :715) ไม่เจอ → 403; กำหนด effectiveUserCode (mapped → user_code ที่ไม่ใช่อีเมล → SUPERADMIN ถ้า ADMIN → admin ถ้า demo → 0001); ถ้าผู้ใช้นั้นยังไม่มีใน sml_user_list จะสร้างให้ (level 9 ถ้า ADMIN, :774) และเพิ่มสิทธิ์เข้าบริษัทถ้ายังไม่มี; จากนั้นเรียก tokens.IssueSession (:808) ออก access/refresh token ของบริษัทนั้นพร้อม level (ADMIN = 9) (JWT session token และการเพิกถอน (JWT + revocation store)). ผลคือ ช่องทางออก JWT นอกเหนือ /auth/login (ขั้นตอน login / เลือกบริษัท / refresh / logout (Identity login flow)).
  9. สมาชิก: GET /workspace/members?database= คืนสมาชิก + สถานะ LINE + รายชื่อ sml_users ที่ active (ให้แอดมินเลือกผูก); POST /workspace/members (:972) และ DELETE /workspace/members (:1096) ต้องผ่าน isDatabaseAdmin (:839) — ตรวจจาก role ในตารางสมาชิก / owner_email / user_level >= 9 / รหัส SUPERADMIN|admin ของ actor_email/actor_user_code ที่ผู้เรียกระบุมาเอง (ไม่ใช่จาก token). Upsert สร้าง mapped_user_code (ค่า default SUPERADMIN ถ้า ADMIN, 0001 ถ้า MEMBER), สร้าง sml_user_list ถ้ายังไม่มี (:1052), เพิ่มสิทธิ์เข้าบริษัท และซิงก์อีเมลไป erp_user ในฐานบริษัท. ลบสมาชิกลบแค่แถวใน sml_google_workspace_member (ไม่ลบ sml_user_list / สิทธิ์เข้าบริษัท).
  10. GET /workspace/my-screen-rights (:1195): ถ้า role=ADMIN ที่ส่งมา หรือ (ไม่ใช่ MEMBER และ isDatabaseAdmin) → {"is_admin":true,"menus":[]}; ไม่งั้นหา mapped_user_code จากอีเมลแล้วรวม image_file ของ user + กลุ่มโดย รวมรายรหัสเมนู (OR ต่อเมนู) ด้วย loadMergedMenuRightsForUser (:1129) → {"is_admin":false,"menus":[...]} (สิทธิ์เมนูและ middleware RequireRight (Menu rights / permission)).
  11. LINE: GET /workspace/line-binding (:1256) แปลงอีเมล ↔ line_user_id (จาก sml_google_account ก่อน แล้ว sml_user_list) พร้อมรายชื่อพนักงานและสถานะผูก LINE ของบริษัท; POST /workspace/line-binding (:1368) ผูก/ยกเลิก (unbind) — ผูกได้เมื่อ actor_email == email (ผูกของตัวเอง) หรือผู้เรียกเป็น Database Admin; การผูกใหม่จะล้าง line_user_id เดิมที่ซ้ำกับบัญชีอื่น แล้วเขียนทั้ง sml_google_account, sml_user_list และ erp_user (เพิ่มคอลัมน์ line_user_id) ในฐานบริษัท.

ข้อควรระวัง

  • ความเชื่อใจอยู่ที่กุญแจ ADMIN_API_KEY เท่านั้น: ใครถือกุญแจนี้ส่ง email/user_code/is_demo=true อะไรก็ได้ แล้ว select-db จะออก JWT ของบริษัทนั้น (ระดับ 9 ถ้า ADMIN) — ไม่มีการตรวจ Google token และ actor_* เป็นค่าที่ผู้เรียกอ้างเอง. ผู้เรียกจริงควรเป็นเซิร์ฟเวอร์ฝั่งหน้าเว็บที่ตรวจ Google แล้วเท่านั้น ห้ามเปิดกุญแจให้เบราว์เซอร์.
  • โค้ดตั้งรหัสผ่านเริ่มต้นแบบ plaintext ให้ผู้ใช้ที่สร้างอัตโนมัติ (select-db :774, upsert member :1052) และ erp_user ของบริษัทใหม่ (:517,523) — ค่ารหัสฝังในโค้ด (ไม่คัดลอกที่นี่) ผู้ใช้เหล่านี้จึงล็อกอินผ่าน /auth/login ด้วยรหัสเริ่มต้นได้ถ้ารู้ชื่อผู้ใช้; ควรตรวจ/บังคับเปลี่ยน (ยังไม่ยืนยันว่ามีขั้นตอนบังคับเปลี่ยนที่อื่น).
  • การสร้างบริษัทไม่ atomic: ถ้า CREATE DATABASE สำเร็จแต่ clone ล้ม ฐานจริงค้างอยู่โดยไม่มีแถวลงทะเบียน (ไม่พบขั้นตอน rollback/DROP ในโค้ด).
  • ฐานแม่แบบถูกกำหนดตายตัวเป็น smldemo ของ Provider ที่เรียก (:396) — ถ้า Provider นั้นไม่มีฐานนี้ ฟังก์ชันคืน error (:398) และการสร้างบริษัทล้ม (ยังไม่ยืนยันพฤติกรรมตอนเปิด pool ฐานที่ไม่มีอยู่จริง).
  • ทุก handler รัน CREATE TABLE/ALTER TABLE IF NOT EXISTS ซ้ำทุกคำขอ และกลืน error — ถ้าผู้ใช้ DB ไม่มีสิทธิ์ DDL ตารางจะไม่ถูกสร้างและ query ถัดไปล้มเงียบ ๆ (ตัวจัดการ connection pool ต่อฐานข้อมูล (PoolManager)).
  • ensureWorkspaceTables ยังถูกเรียกจาก syncUserAllowedDatabases ของ Admin API (Admin API สำหรับจัดการ Provider/ผู้ใช้/ฐานข้อมูล/สิทธิ์เมนู (Admin API)) จึงเป็นตาราง "ของระบบ" ไม่ใช่เฉพาะ route กลุ่มนี้.

ที่มาในโค้ด

  • backend/internal/adapter/inbound/http/admin/workspace_google.go:18-76 — regex ชื่อฐาน, reservedDBs, ตารางที่สร้าง.
  • backend/internal/adapter/inbound/http/admin/workspace_google.go:91-261 — queryAccessibleWorkspaceDatabases (3 แหล่ง).
  • backend/internal/adapter/inbound/http/admin/workspace_google.go:263-393 — google-login, list, ตรวจชื่อ.
  • backend/internal/adapter/inbound/http/admin/workspace_google.go:395-672 — clone แม่แบบ + สร้างบริษัท.
  • backend/internal/adapter/inbound/http/admin/workspace_google.go:674-837 — select-db + ออก token.
  • backend/internal/adapter/inbound/http/admin/workspace_google.go:839-1128 — isDatabaseAdmin, สมาชิก.
  • backend/internal/adapter/inbound/http/admin/workspace_google.go:1129-1481 — สิทธิ์หน้าจอ, LINE binding.
  • backend/internal/adapter/inbound/http/router.go:237-247 — route ทั้ง 11 ตัว.

ดูเพิ่ม

ภาพรวม Backend Go ของ SML Ai Account (Backend overview) · Admin API สำหรับจัดการ Provider/ผู้ใช้/ฐานข้อมูล/สิทธิ์เมนู (Admin API) · โมเดลผู้เช่าหลายราย: Tenant, Provider, Main DB, Company (Multi-tenant model) · JWT session token และการเพิกถอน (JWT + revocation store) · สิทธิ์เมนูและ middleware RequireRight (Menu rights / permission) · ขั้นตอน login / เลือกบริษัท / refresh / logout (Identity login flow) · ADR-0003 ข้อมูลผู้ใช้และสิทธิ์อยู่ใน DB ของแต่ละผู้เช่า ไม่ใช่ control-plane · สารบัญ ADR ของ backend (Architecture Decision Records)