create-fullstack-scaffold 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fullstack-scaffold",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-fullstack-scaffold": "dist/cli/index.js"
@@ -0,0 +1,110 @@
1
+ # SaaS 多租户形态指南(saas preset)
2
+
3
+ > 面向:用 `--preset saas` 生成多租户 SaaS 应用的开发者。
4
+ > 版本:v0.6.0 起全部链路真实可用(此前仅租户记录 CRUD)。
5
+
6
+ ## 这套形态解决什么问题
7
+
8
+ 给"一个平台服务多个客户组织"的场景提供开箱即用的骨架:
9
+
10
+ - 平台侧:超级管理员管理所有租户(CRUD/套餐/配额字段)
11
+ - 租户侧:每个租户有自己的管理员、成员、角色、数据
12
+ - 成员关系:邮件邀请(7 天 token 链接)→ 接受入组 → 按角色授权
13
+ - 数据隔离:业务数据带 `tenant_id`,按请求上下文过滤
14
+
15
+ ## 适用场景
16
+
17
+ | 场景 | 例子 | 用到的能力 |
18
+ | -------------- | --------------------------- | -------------------------- |
19
+ | B2B 工具站 | 团队协作工具、项目管理 SaaS | 成员邀请 + 角色 + 数据隔离 |
20
+ | 内容平台多组织 | 各机构独立发内容的内容站 | 租户 + ISR 内容页 + 隔离 |
21
+ | 内部多部门系统 | 集团内各部门各看各的数据 | 租户识别 + 配额 |
22
+
23
+ ## 不适用场景(诚实边界)
24
+
25
+ - **面向 C 端个人用户**(每人一个"空间"而非"组织"):个人维度用 `user_id` 即可,租户模型是过度设计 → 用 `todo-app`/`forum` preset
26
+ - **需要真订阅计费**(Stripe/发票/试用期):本模板只有 plan 字段 + 配额执行,**没有**账单表和支付集成;接 Stripe 属独立工程
27
+ - **需要组织级 SSO/SCIM**:认证是本地账号(bcrypt + JWT),没有 SAML/OIDC
28
+ - **强合规多租户**(SOC2 级审计/数据驻留):审计日志是平台级的,尚无租户维度切分
29
+
30
+ ## 核心概念与链路
31
+
32
+ ```
33
+ 平台超管(super_admin) 租户管理员(tenant_admin) 普通成员(tenant_member)
34
+ │ │ │
35
+ ├─ 平台级 CRUD 所有租户 ├─ 邀请成员(邮件+角色) ├─ 按角色权限访问数据
36
+ ├─ 租户套餐/配额字段 ├─ 改成员角色/移除 │
37
+ └─ CLI 管理 └─ 租户内自定义角色(按套餐限额) │
38
+ ```
39
+
40
+ **租户开通**(事务,三步原子):`POST /api/tenants` → 建租户记录 + 播种 3 个系统角色
41
+ (admin 全权限 / member 读写 / guest 只读)+ owner 以 tenant_admin 入组。
42
+
43
+ **邀请加入**:`POST /tenants/:id/members/invite`(生成 7 天 token)→ 受邀人打开
44
+ `/tenant/invite/:token`(公开页,脱敏详情)→ 登录后 Accept → 事务内标记 accepted + 建成员。
45
+ 两次配额校验:邀请时 + 接受时(防邀请期内名额被占满)。
46
+
47
+ **租户识别**:请求头 `X-Tenant-Slug` 或子域名(`xxx.example.com`)。中间件为
48
+ **可选上下文**语义:无标识 → 全局模式放行(平台级 API 照常);slug 不存在 → 404。
49
+
50
+ **配额执行**(真实拦截,非展示):
51
+
52
+ - 成员数:`tenants.max_users`,邀请与接受两处校验,超限 400
53
+ - 自定义角色数:`PLAN_ROLE_LIMITS`(free 3 / starter 5 / pro 10 / enterprise ∞)
54
+
55
+ ## 快速上手
56
+
57
+ ```bash
58
+ npx create-fullstack-scaffold my-saas --preset saas
59
+ cd my-saas && npm install && npx drizzle-kit push
60
+ npm run dev # 首次启动自动建表+种子
61
+
62
+ # 平台超管(首启日志会打印一次凭据提醒)
63
+ # 账号 superadmin / 密码 admin123 —— 上线前务必改密
64
+
65
+ # 1) 超管开租户(或注册账号后自助登录建)
66
+ curl -X POST localhost:5173/api/tenants \
67
+ -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
68
+ -d '{"name":"Acme","slug":"acme","plan":"pro"}'
69
+
70
+ # 2) 邀请成员(返回 7 天邀请链接)
71
+ curl -X POST localhost:5173/api/tenants/<id>/members/invite \
72
+ -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
73
+ -d '{"email":"dev@acme.io","roleId":"<role-id>"}'
74
+
75
+ # 3) 受邀人打开 http://localhost:5173/tenant/invite/<token> → Accept → 进租户控制台
76
+ # 控制台入口:http://localhost:5173/tenant/login
77
+ ```
78
+
79
+ CLI(免登录,平台管理视角):
80
+
81
+ ```bash
82
+ npx cfs tenant list # 租户列表
83
+ npx cfs tenant create --name Acme --slug acme --plan pro
84
+ npx cfs tenant roles --id <id> # 看角色(拿 role-id)
85
+ npx cfs tenant members --id <id> # 成员清单
86
+ npx cfs tenant invite --id <id> --email dev@acme.io --role-id <role-id>
87
+ ```
88
+
89
+ ## 数据模型
90
+
91
+ | 表 | 用途 | 关键列 |
92
+ | -------------------- | ------------ | --------------------------------------------------------------- |
93
+ | `tenants` | 租户 | slug(唯一)/plan/max_users/status |
94
+ | `tenant_roles` | 租户内角色 | tenant_id+code 唯一,permissions(JSON),is_system |
95
+ | `tenant_members` | 成员归属 | user_id+tenant_id 唯一,软删(status='left') |
96
+ | `tenant_invitations` | 邀请 | token 唯一,7 天过期,状态机 pending→accepted/expired/cancelled |
97
+ | `todos.tenant_id` | 业务数据归属 | nullable(无 tenant preset 的 preset 恒 null) |
98
+
99
+ ## 已知边界(改进路线)
100
+
101
+ 1. 成员 Account 展示依赖 `developers.id === tenant_members.user_id` 直等;dev token 用户显示原始 id
102
+ 2. 移动端 375px 下租户控制台侧栏不收起
103
+ 3. `contents` 表尚未接 tenant_id(内容模块在 saas preset 内仍是全局数据)
104
+ 4. 审计日志无租户维度
105
+
106
+ ## 部署注意
107
+
108
+ - **必须设置 `AUTH_SECRET_KEY`**(任意强随机串)——生产默认 key 会被启动警告
109
+ - `superadmin/admin123` 种子只在首启空库时创建,上线前立即改密
110
+ - 租户控制台是独立入口 `/tenant/*`(tenant.html),生产部署确认 `dist/client/tenant.html` 存在
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "biomimic-todo-app",
3
3
  "private": true,
4
- "version": "0.6.3",
4
+ "version": "0.6.5",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "biomimic": "./dist/cli/index.js"
@@ -105,13 +105,13 @@ export async function closeDb(): Promise<void> {
105
105
  async function stampJournalIfPushBuilt(migrationsFolder: string): Promise<void> {
106
106
  if (!_client || !('execute' in _client)) return
107
107
 
108
- // todos 表在(= 当前 schema 形态、由 push 建成)。只查 todos——
109
- // tenants 仅存在于含 tenant 模块的 preset,用它做条件会漏掉全部
110
- // tenant preset 的库(0.6.2 首次部署七杀五的根因)
111
- const shape = await _client.execute(
112
- "SELECT COUNT(*) AS c FROM sqlite_master WHERE type='table' AND name='todos'"
108
+ // 普适判定:库里有任何业务表 && 账本为空 = push 建库形态。
109
+ // 不能锚定具体表名——preset 组合差异大(forum/plugin todos、
110
+ // 多数 preset 无 tenants,0.6.2/0.6.3 两次部署事故均源于此)
111
+ const bizTables = await _client.execute(
112
+ "SELECT COUNT(*) AS c FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != '__drizzle_migrations'"
113
113
  )
114
- if (Number(shape.rows[0]?.c ?? 0) < 1) return
114
+ if (Number(bizTables.rows[0]?.c ?? 0) < 1) return
115
115
 
116
116
  const journalTable = await _client.execute(
117
117
  "SELECT COUNT(*) AS c FROM sqlite_master WHERE type='table' AND name='__drizzle_migrations'"