Plugins › Multi-tenant
เริ่มต้น
เนื้อหา
Plugins
การดูแลระบบ
สูตรสำเร็จ
Multi-tenant
หน้านี้สอนอะไร
การใช้ CMS ตัวเดียวกับหลายเว็บหรือหลายลูกค้า: แต่ละ tenant มีเนื้อหา สมาชิก และการตั้งค่าของตัวเอง มีตัวสลับ tenant ในหน้า admin และ frontend อ่านเฉพาะ tenant ของตัวเอง
ควรอ่านก่อน: Access control, บทบาท และ Plugins
@easy-cms/plugin-multi-tenant เพิ่ม tenant ให้ CMS ตัวเดียว ฐานข้อมูลเดียว และ admin ชุดเดียว tenant คือแบรนด์ เว็บ สาขา
หรือลูกค้า แต่ละ tenant มีเอกสารของตัวเองใน collection ที่คุณระบุ มีค่าของตัวเองใน global ที่คุณระบุ และมีสมาชิกที่มีบทบาทใน tenant นั้น
ส่วนที่เหลือใช้ร่วมกัน
เหมาะกับบริษัทที่มีหลายแบรนด์หรือหลายเว็บ และ agency ที่ดูแลเว็บของลูกค้า ทีมทำงานได้ทุก tenant ส่วนลูกค้าแต่ละรายเข้าได้เฉพาะของตัวเอง ถ้าลูกค้าห้ามใช้ฐานข้อมูลร่วมกันเลย ให้ใช้ CMS แยกตัวต่อลูกค้าแทน
ติดตั้ง
npm install @easy-cms/plugin-multi-tenantpnpm add @easy-cms/plugin-multi-tenantyarn add @easy-cms/plugin-multi-tenantbun add @easy-cms/plugin-multi-tenantimport { multiTenantPlugin } from '@easy-cms/plugin-multi-tenant'
export default defineConfig({
// …
auth: { rbac: true }, // ไม่บังคับ: บทบาทจากหน้า admin ให้แยกตาม tenant
plugins: [
multiTenantPlugin({
collections: ['posts', 'pages', 'media'], // เอกสารแต่ละชิ้นเป็นของ tenant เดียว
globals: ['site-settings'], // ค่าแยกต่อ tenant
}),
],
})plugin เพิ่มให้:
- ตั้งค่า › ผู้ใช้และสิทธิ์ › Tenants: ชื่อ slug และโดเมนของแต่ละ tenant
- field
tenantในทุก collection ที่ระบุ ใส่ให้เองจาก tenant ที่กำลังทำงานอยู่ ถ้ามีmediaและupload.foldersโฟลเดอร์สื่อก็เป็นของ tenant ด้วย - ตัวสลับ tenant ด้านบนของเมนู admin สำหรับคนที่อยู่มากกว่าหนึ่ง tenant
- ตั้งค่า › ผู้ใช้และสิทธิ์ › สมาชิก: คนใน tenant ที่เลือกและบทบาทของแต่ละคน
tenantsในผู้ใช้: tenant ที่แต่ละคนอยู่และบทบาทในแต่ละ tenant
จากนั้นสร้าง migration ตามปกติ npx easy-cms migrate:create tenants ถ้าเว็บมีเนื้อหาอยู่แล้ว ให้ย้ายไปไว้ใน tenant หนึ่งครั้งเดียว:
npx easy-cms tenants:assign brand-aใครเห็นอะไร
| ใคร | เห็นและแก้ไขได้ |
|---|---|
ผู้ใช้ที่เข้าได้ทุก tenant (ค่าเริ่มต้น: role admin) | ทุก tenant หรือ tenant ที่เลือก รวมถึง Tenants, ตั้งค่า, สำรองข้อมูล และบทบาท |
| สมาชิก | tenant ที่กำลังทำงาน ด้วยบทบาทใน tenant นั้น: หนึ่งใน tenant ของเขา เลือกจากตัวสลับ |
admin ของ tenant (role admin ใน tenant นั้น) | เนื้อหาและสมาชิกของ tenant นั้น ไม่รวมบทบาท สำรองข้อมูล อีเมล และ tenant อื่น |
| ผู้เยี่ยมชม | tenant ที่ request ระบุ ถ้าไม่ระบุจะไม่เห็นอะไร |
- เอกสารใหม่ได้ tenant ที่ request กำลังทำงานอยู่ เฉพาะผู้ใช้ที่เข้าได้ทุก tenant ที่เลือก tenant อื่นหรือย้ายเอกสารได้
- relationship และ upload ที่ชี้ไป collection ของ tenant เลือกและบันทึกได้เฉพาะเอกสารของ tenant เดียวกัน เอกสารที่ plugin สร้างโดยไม่มี tenant (ข้อมูลที่ส่งจากฟอร์ม, redirect ของหน้า) ได้ tenant ตามเอกสารที่มันชี้ไป
- โฟลเดอร์สื่อเป็นของ tenant field upload ที่ตั้ง
folder: 'banners'จะเปิดโฟลเดอร์bannersของแต่ละ tenant - ค่าที่ unique รวมถึง slug ห้ามซ้ำแค่ภายใน tenant สองแบรนด์จึงมี
/posts/helloได้ทั้งคู่ - เพิ่มคนที่ สมาชิก ด้วยอีเมล บัญชีหนึ่งอยู่ได้หลาย tenant คนใหม่จะได้อีเมลให้ตั้งรหัสผ่านเมื่อตั้งค่าอีเมลแล้ว admin ของ tenant แก้บัญชีของคนอื่นไม่ได้ แก้ได้แค่การเป็นสมาชิก
- API key จำ tenant ที่สร้างไว้ตลอด
- Audit log เก็บ tenant ของแต่ละ entry admin ของ tenant เห็นของ tenant ตัวเอง ผู้ที่เข้าได้ทุก tenant เห็นทั้งหมด หรือของ tenant ที่เลือก
- ตอนแสดงทุก tenant หน้ารายการมีคอลัมน์ Tenant และตัวสลับใช้กรองเหลือ tenant เดียว
ใช้กับ plugin อื่น
ใส่ collection ของ plugin เหล่านั้นใน collections เพื่อแยกต่อ tenant และวาง multiTenantPlugin ไว้หลัง plugin ที่สร้าง collection:
plugins: [
nestedDocsPlugin({ collections: ['pages'] }),
redirectsPlugin({ collections: ['pages'], url }),
formBuilderPlugin(),
multiTenantPlugin({
collections: ['pages', 'redirects', 'forms', 'form-submissions', 'media'],
}),
]- หน้าย่อย: path ห้ามซ้ำแค่ภายใน tenant แต่ละ tenant จึงมี
/aboutได้ ส่ง tenant ให้ helper:findByPath(cms, 'pages', path, { context })และgetTree(cms, 'pages', { context }) - Redirects: แต่ละ tenant มีของตัวเอง หน้าที่ย้ายจะได้ redirect ใน tenant ของมัน
resolveRedirect(cms, url, { context })ใช้ของ tenant ของ request - ฟอร์ม:
<easy-form>และ/api/cms/form/<slug>หาฟอร์มของ tenant ของ request (โดเมนหรือ header) และข้อมูลที่ส่งมาเป็นของ tenant นั้น - SEO: ดูด้านล่าง
ในที่นี้ context คือ await tenantContext(cms, { host }) หรือ (await cms.forRequest(request)).context
Frontend
request ระบุ tenant ตามลำดับนี้:
- header
x-easy-cms-tenantด้วย slug หรือ id ของ tenant ?tenant=ใน URL- cookie ของ admin (ตัวสลับ)
- โดเมน: tenant ที่มี host ของ request อยู่ใน Domains
// แอปอื่นหรือเว็บ static
const posts = await fetch('https://cms.example.com/api/cms/posts', {
headers: { 'x-easy-cms-tenant': 'brand-a' },
}).then((r) => r.json())ในหน้า Nuxt หรือ Next.js อ่านด้วย Local API ใน tenant ของโดเมนของหน้า:
import { tenantContext } from '@easy-cms/plugin-multi-tenant'
const context = await tenantContext(cms, { host: request.headers.get('host') ?? '' })
const posts = await cms.find('posts', { overrideAccess: false, user: null, context })
const site = await cms.findGlobal('site-settings', { overrideAccess: false, user: null, context })tenantContext(cms, { slug }) ใช้กับ slug จาก URL ได้ เช่น route /[tenant]/[slug] ส่วน cms.forRequest(request)
คืนผู้ใช้และ context ของ request แบบเดียวกับ REST
เมื่อใช้ plugin SEO /sitemap.xml และ /llms.txt แสดงหน้าของ tenant ตามโดเมนที่ขอเข้ามา และ helper ของมันรับ context
เดียวกัน: sitemap(cms, { context: await tenantContext(cms, { host }) })
request ที่ไม่ระบุ tenant จะไม่พบอะไรใน collection ของ tenant ถ้าตั้ง publicReads: 'all' จะพบของทุก tenant แทน
ตัวเลือก
| ตัวเลือก | ค่าเริ่มต้น | |
|---|---|---|
collections | — | ต้องระบุ collection ที่เอกสารเป็นของ tenant เดียว |
globals | [] | global ที่มีค่าแยกต่อ tenant |
tenantsSlug | tenants | collection ของ tenant ประกาศเองเพื่อเพิ่ม field ได้ |
userHasAccessToAllTenants | role admin | (user) => boolean: ใครเห็นทุก tenant และส่วนของระบบ |
publicReads | none | all: request ที่ไม่ระบุ tenant อ่านของทุก tenant |
header | x-easy-cms-tenant | header ที่ใช้ระบุ tenant |
cookie | ecms-tenant | cookie ที่ admin ใช้จำ tenant ที่เลือก |
ลบ tenant
การลบ tenant ที่ ตั้งค่า › ผู้ใช้และสิทธิ์ › Tenants จะลบเอกสารและไฟล์ใน collection ของ tenant นั้น และเอา tenant ออกจากรายการของสมาชิก หน้า admin บอกว่าจะหายไปเท่าไร และให้พิมพ์ชื่อ tenant ก่อนลบ ย้อนกลับไม่ได้ สำรองข้อมูลก่อน
ทำงานอย่างไร
plugin ใช้ส่วนของ core ที่คุณใช้เองได้:
onRequestใน config หาcontextของแต่ละ request (ในที่นี้คือ tenant) และผู้ใช้ใน context นั้น (บทบาทใน tenant,scoped) กฎสิทธิ์ hook และfilterOptionsได้รับcontextและ Local API รับเป็นตัวเลือกได้- ผู้ใช้ที่
scopedเป็น admin เฉพาะส่วนของตัวเอง ตั้งค่า สำรองข้อมูล บทบาท และ API key ของคนอื่นต้องเป็น admin ของทั้งระบบ - global ที่มี
scopeเก็บค่าแยกตาม scope uniqueWithinใช้ได้กับทุก field ที่ unique พร้อม unique index ต่อ scope ในฐานข้อมูลadmin.switcherเพิ่มตัวเลือกด้านบนของเมนูaudit.scopeเก็บส่วนของเว็บของแต่ละ entry ใน audit logadmin.confirmDelete,admin.defaultValue,admin.columnและadmin.allowCreateปรับการลบ เอกสารใหม่ และหน้ารายการใน adminfilterOptionsของ field upload จำกัดไฟล์ที่เลือกได้cms.uniqueScope()บอก plugin ว่า field ที่ unique ห้ามซ้ำภายในอะไรสำหรับ request นั้น
ยังไม่มี: สำรองหรือ export ข้อมูลทีละ tenant, webhook ต่อ tenant, tenant ที่สมัครเอง และ single sign-on ที่ใส่คนเข้า tenant ตามโดเมนของอีเมล
ขั้นต่อไป
- บทบาท: แต่ละบทบาททำอะไรได้ สมาชิกได้บทบาทหนึ่งต่อ tenant
- Access control: กฎของคุณเองเห็น
contextของ request - API keys: key สำหรับสคริปต์ของ tenant เดียว