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 ที่ถือกุญแจ) ส่งมา.
ทำงานอย่างไร
- ตารางที่ใช้ (สร้างด้วย
ensureWorkspaceTablesทุกครั้งที่ handler เรียก,workspace_google.go:36-76; ผลถูกกลืน):sml_google_account(PKemail,google_sub,display_name,picture_url,line_user_id,line_display_name,line_linked_at),sml_google_workspace_db(PKdatabase_name,data_code,company_name,owner_email,provider_code),sml_google_workspace_member(PKdatabase_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). POST /workspace/google-login(:263): ต้องมีemailที่มี@; upsertsml_google_account(เก็บgoogle_sub/ชื่อ/รูปเฉพาะค่าที่ไม่ว่าง) แล้วคืนรายการบริษัทที่เข้าถึงได้.queryAccessibleWorkspaceDatabases(:91) รวมบริษัทจาก 3 แหล่ง แล้ว merge ตามชื่อฐาน (role ADMIN ชนะ), ข้ามชื่อขึ้นต้นsmlerpmain:- (1)
sml_google_workspace_memberตามอีเมล — roleMEMBERต้องผูก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 เป็นคนอื่นจะไม่รั่วให้ เว้นแต่ผู้ใช้ถูกเชิญ/ผูกอยู่ในตารางสมาชิก.
- (1)
GET /workspace/databasesใช้ฟังก์ชันเดียวกัน (queryemail,user_code);GET /workspace/check-db-name?name=เรียกcheckDatabaseNameAvailable(:343).- กติกาชื่อฐานข้อมูล (
: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ของเซิร์ฟเวอร์ → ไม่ผ่าน = 409DB_NAME_TAKEN. POST /workspace/databases(:533) สร้างบริษัทใหม่ 3 ขั้น:- ต้องมี
owner_email(หรือemail; ถ้าไม่มีแต่มีuser_codeจะใช้user_codeพิมพ์เล็กแทน) ไม่งั้น 400MISSING_OWNER; ชื่อบริษัทว่าง =กิจการ <ชื่อฐานพิมพ์ใหญ่>;data_codeว่าง = ชื่อฐานพิมพ์ใหญ่. CREATE DATABASE ... ENCODING 'UTF8'บนฐานpostgresของ Provider (:597) แล้วcloneEmptySchemaFromTemplateภายใต้ timeout 45 วินาที (:604).- ลงทะเบียนใน Main DB: แถว
sml_database_list(groupMAIN), สิทธิ์เข้าบริษัทให้SUPERADMIN,adminและuser_codeที่ส่งมา (ถ้าไม่ใช่อีเมล), แถวsml_google_workspace_dbและสมาชิก ADMIN (mapped_user_code=user_codeหรือSUPERADMINถ้าว่าง/เป็นอีเมล). ผลของการลงทะเบียนถูกกลืน (_, _ =).
- ต้องมี
cloneEmptySchemaFromTemplate(:395-531): อ่านโครงตารางทั้งหมดใน schemapublicของฐานแม่แบบsmldemoจาก catalog (pg_attribute,pg_constraint) แล้วCREATE TABLE IF NOT EXISTSทีละตารางในฐานใหม่ (ถ้าพังใช้ DDL แบบพื้นฐานแทน), คัดลอก PRIMARY KEY/UNIQUE, แล้ว seed เฉพาะerp_option(ทศนิยม 2 ตำแหน่ง),erp_company_profile(ชื่อบริษัท สาขา 00000 "สำนักงานใหญ่") และผู้ใช้erp_userSUPERADMIN(level 9) +user_codeของผู้สร้าง — ไม่คัดลอกข้อมูลและไม่คัดลอก index/FK/view/function (คัดเฉพาะ PK/UNIQUE; ส่วนอื่นยังไม่ยืนยันว่ามีที่อื่นสร้างให้หรือไม่).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)).- สมาชิก:
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(ค่า defaultSUPERADMINถ้า ADMIN,0001ถ้า MEMBER), สร้างsml_user_listถ้ายังไม่มี (:1052), เพิ่มสิทธิ์เข้าบริษัท และซิงก์อีเมลไปerp_userในฐานบริษัท. ลบสมาชิกลบแค่แถวในsml_google_workspace_member(ไม่ลบsml_user_list/ สิทธิ์เข้าบริษัท). 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)).- 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)