create-fullstack-scaffold 0.6.4 → 0.6.6
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/README.md
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
Zero-config fullstack app generator with type-safe RPC, 15+ modules, and 8 production-ready presets. One command, zero `.env`, instant `npm run dev`.
|
|
6
6
|
|
|
7
|
+
## Try It & Mission Pack
|
|
8
|
+
|
|
9
|
+
- **Live demo** (fullstack-admin preset): https://demo.lpm1.top
|
|
10
|
+
- **Mission Pack** — copy-paste prompts that make any AI agent build with this scaffold:
|
|
11
|
+
[MISSION-PACK.md](./MISSION-PACK.md) (includes downloadable Skill URLs, task prompts A–D, and hard-won agent tips)
|
|
12
|
+
|
|
7
13
|
## Quick Start
|
|
8
14
|
|
|
9
15
|
```bash
|
package/package.json
CHANGED
|
@@ -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` 存在
|
package/template/package.json
CHANGED
|
@@ -88,7 +88,9 @@ export const useTenantStore = create<TenantState>((set, getState) => ({
|
|
|
88
88
|
try {
|
|
89
89
|
const loginRes = await api<{ token: string }>('/auth/login', {
|
|
90
90
|
method: 'POST',
|
|
91
|
-
|
|
91
|
+
// account + username 双字段:不同 preset 的 LoginSchema 字段名不同
|
|
92
|
+
//(saas 用 account,fullstack-admin 等用 username),双发保证兼容
|
|
93
|
+
body: { account, username: account, password },
|
|
92
94
|
})
|
|
93
95
|
if (!loginRes.success || !loginRes.data?.token) {
|
|
94
96
|
return { ok: false, hasTenant: false, error: 'Invalid credentials' }
|