20 KiB
Person Panel 多用户数据中心与 RSS 实现计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 在现有 Nuxt 4 + Nitro + SQLite + Drizzle + @nuxt/ui 项目上,实现多用户隔离的个人资料/文章/时光机/RSS 收件箱、条目级可见性与分享链接、实例管理员开号与禁用、以及进程内 RSS 定时同步(符合 docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md)。
Architecture: 单体 Nitro API 分 public / me / admin 三层;内容域按 userId 强隔离;可见性三态统一;RSS 抓取与调度放在 server/service/rss + server/plugins 进程内定时器;前端用 Nuxt UI 统一空状态与表单,公开路由与 API 白名单同步放开。
Tech Stack: Nuxt 4.4、Nitro、@nuxt/ui 4.x、Drizzle ORM、SQLite、better-sqlite3、bcryptjs、Zod、(新增)适用于 RSS 解析的轻量 XML 解析库、原生 fetch。
File Structure Map(新建 / 修改)
| 路径 | 职责 |
|---|---|
packages/drizzle-pkg/database/sqlite/schema/auth.ts |
扩展 users:role、status、publicSlug、bioMarkdown、bioVisibility、socialLinksJson、avatarVisibility 等 |
packages/drizzle-pkg/database/sqlite/schema/content.ts(新建) |
posts、timeline_events 表定义 |
packages/drizzle-pkg/database/sqlite/schema/rss.ts(新建) |
rss_feeds、rss_items |
packages/drizzle-pkg/lib/schema/* |
重新导出新建表 |
packages/drizzle-pkg/seed.ts |
Bootstrap 管理员创建(读 env,仅在无 admin 时插入) |
server/constants/visibility.ts(新建) |
Visibility 枚举与 Zod schema |
server/utils/share-token.ts(新建) |
生成/校验 shareToken(crypto 随机 24+ bytes URL-safe) |
server/utils/rss-url.ts(新建) |
SSRF 前置校验:scheme、主机、禁止私网 IP(解析后)、重定向上限策略的入口 |
server/utils/admin-guard.ts(新建) |
requireAdmin(event) |
server/middleware/10.auth-guard.ts |
配合扩展 allowlist;getCurrent 已登录但用户 disabled → 401 + 清 cookie(见 Task 3) |
server/utils/auth-api-routes.ts |
增加 /api/public 前缀规则、/api/admin 是否走会话(admin 需登录但不 allowlist 全开放) |
server/service/auth/index.ts |
getCurrentUser join users.status,active 以外返回 null;MinimalUser 扩展 role |
server/service/auth/context.ts |
MinimalUser 类型扩展 |
server/api/auth/me.get.ts |
返回 role、publicSlug 等安全字段 |
server/api/admin/users.post.ts 等 |
管理员创建/列表/禁用 |
server/api/me/posts.*.ts |
文章 CRUD |
server/api/me/timeline.*.ts |
时间线 CRUD |
server/api/me/profile.put.ts、me/profile.get.ts |
资料读写 |
server/api/me/rss/feeds.*.ts、me/rss/items.*.ts、me/rss/sync.post.ts |
订阅与同步触发 |
server/api/public/profile/[publicSlug].get.ts |
聚合公开字段与 public 内容列表 |
server/api/public/unlisted.get.ts 或 [publicSlug]/[token].get.ts |
按 slug+token 解析条目类型并返回 |
server/service/rss/fetch.ts(新建) |
fetch feed、解析、入库、去重 |
server/service/rss/scheduler.ts(新建) |
到期扫描、并发池、写回 lastError |
server/plugins/04.rss-scheduler.ts(新建) |
nitroApp.hooks.hook("ready", ...) 注册 setInterval + 启动补跑 |
app/utils/auth-routes.ts |
PUBLIC_ROUTE_PREFIXES: /@, /p/;移除或条件化 /register |
app/pages/@/[publicSlug]/index.vue(新建) |
公开主页 |
app/pages/p/[publicSlug]/t/[shareToken].vue(新建) |
仅链接阅读页 |
app/pages/me/** 或 app/pages/dashboard/** |
后台列表与编辑(按你偏好的前缀统一) |
app/components/AppShell.vue |
侧栏导航、空状态入口、admin 菜单 |
server/service/auth/index.test.ts 或新测试文件 |
扩展测试 |
Task 1: Drizzle schema — 用户扩展与内容/RSS 表
Files:
-
Modify:
packages/drizzle-pkg/database/sqlite/schema/auth.ts -
Create:
packages/drizzle-pkg/database/sqlite/schema/content.ts -
Create:
packages/drizzle-pkg/database/sqlite/schema/rss.ts -
Modify:
packages/drizzle-pkg/lib/schema/auth.ts(若需聚合导出) -
Create:
packages/drizzle-pkg/lib/schema/content.ts、rss.ts导出 -
Step 1: 扩展
users列
在 auth.ts 的 users 表增加(类型与默认值与迁移一致):
-
role:text,默认'user',非空 -
status:text,默认'active',非空 -
publicSlug:text,唯一索引(nullable 允许首登后填写) -
bioMarkdown:text,可选 -
bioVisibility:text,默认'private' -
socialLinksJson:text,默认'[]'(存 JSON 字符串) -
avatarVisibility:text,默认'private' -
Step 2: 定义
posts
字段建议:id、userId、title、slug、bodyMarkdown、excerpt、coverUrl、tagsJson、publishedAt、visibility、shareToken、createdAt、updatedAt;唯一索引 (userId, slug)。
- Step 3: 定义
timeline_events
字段:id、userId、occurredOn(timestamp)、title、bodyMarkdown、linkUrl、visibility、shareToken、createdAt、updatedAt。
- Step 4: 定义
rss_feeds/rss_items
rss_feeds:id、userId、feedUrl(唯一 per user)、title、siteUrl、lastFetchedAt、lastError、pollIntervalMinutes 默认 null(用全局配置)、createdAt。
rss_items:id、userId、feedId、guid、canonicalUrl、title、summary、contentSnippet、author、publishedAt、visibility 默认 private、shareToken、createdAt;索引 (feedId, guid)、(userId, canonicalUrl) 辅助去重。
- Step 5: 生成并应用迁移
Run:
cd /home/dash/projects/person-panel && bun run db:generate -- add-multitenant-content-rss
bun run db:migrate
Expected: 生成新 SQL 迁移且无报错。
- Step 6: Commit
git add packages/drizzle-pkg
git commit -m "feat(db): add multitenant profile, posts, timeline, and rss tables"
Task 2: Bootstrap 首个管理员(env + seed)
Files:
-
Modify:
packages/drizzle-pkg/seed.ts -
Modify:
packages/drizzle-pkg/env.ts(若集中定义 env) -
Step 1: 在
seed.ts增加幂等 bootstrap
逻辑:
- 查询是否存在
users.role = 'admin'。 - 若不存在,读取
process.env.BOOTSTRAP_ADMIN_USERNAME与BOOTSTRAP_ADMIN_PASSWORD;缺一则 跳过并 log warn(避免误创建)。 - 使用与线上一致的
bcrypthash 流程插入管理员:role='admin'、status='active'、publicSlug可由 username 派生(小写+校验)或留空。
- Step 2: 文档化运维步骤
在 docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md 已描述;在 README 或 .env.example 增加变量名(若仓库允许)。
- Step 3: 手动验证
Run:
BOOTSTRAP_ADMIN_USERNAME=admin BOOTSTRAP_ADMIN_PASSWORD=adminadmin bun run db:seed
Expected: 第二次执行不应再创建第二个 admin(幂等)。
- Step 4: Commit
git add packages/drizzle-pkg
git commit -m "feat(db): bootstrap first admin from environment"
Task 3: 会话用户加载 role/status,禁用用户拒绝访问
Files:
-
Modify:
server/service/auth/index.ts -
Modify:
server/service/auth/context.ts -
Modify:
server/api/auth/me.get.ts -
Step 1: 扩展
getCurrentUser查询列
select 增加 users.role、users.status,where 保持 session 有效条件,并加 eq(users.status, 'active')(或在内存判断)。
若 status !== 'active':删除该 session 或保留 session 但返回 null(推荐删除 session + null,避免僵尸会话)。
- Step 2: 扩展
MinimalUser
{ id, username, role }。
- Step 3:
/api/auth/me返回
user: { id, username, role, publicSlug }(publicSlug 来自 DB)。
- Step 4: 运行现有测试
Run:
cd /home/dash/projects/person-panel && bun test server/service/auth/index.test.ts
Expected: PASS;如失败则更新测试夹具。
- Step 5: Commit
git add server/service/auth server/api/auth/me.get.ts
git commit -m "feat(auth): expose role and enforce disabled users"
Task 4: API 白名单与 public 前缀
Files:
-
Modify:
server/utils/auth-api-routes.ts -
Modify:
server/middleware/10.auth-guard.ts(若需 trace) -
Step 1: 实现前缀 allowlist
增加函数:isPublicApiPath(path: string): boolean,当 path.startsWith('/api/public/') 时为 true(含 GET/POST 中只读方法,禁止开放写)。
将 isAllowlistedApiPath 改为:rule match OR isPublicApiPath(path)。
保留 /api/auth/login、/api/auth/register(后续 Task 会收敛 register)。
- Step 2: 手动探测
启动 dev 后 curl -i http://localhost:3000/api/public/health(若实现)应 200 无 cookie。
- Step 3: Commit
git add server/utils/auth-api-routes.ts server/middleware/10.auth-guard.ts
git commit -m "feat(api): allow unauthenticated access to /api/public"
Task 5: 管理员守卫 + 简易限流
Files:
-
Create:
server/utils/admin-guard.ts -
Create:
server/utils/simple-rate-limit.ts(可选内联于 admin 首行) -
Step 1:
requireAdmin
import type { H3Event } from "h3";
export async function requireAdmin(event: H3Event) {
const user = await event.context.auth.requireUser();
if (user.role !== "admin") {
throw createError({ statusCode: 403, statusMessage: "Forbidden" });
}
return user;
}
- Step 2: 内存限流(每 IP 每分钟 60 次 admin 写操作)
使用 event.node.req.socket.remoteAddress 为 key,Map 存计数与时间窗;超限 429。
- Step 3: Commit
git add server/utils/admin-guard.ts server/utils/simple-rate-limit.ts
git commit -m "feat(admin): require admin role and basic rate limiting"
Task 6: Admin 用户 API
Files:
-
Create:
server/api/admin/users.post.ts -
Create:
server/api/admin/users.get.ts -
Create:
server/api/admin/users/[id].patch.ts -
Step 1: POST 创建用户
Body:{ username, password, email? };Zod 校验;role 固定 user;status active;密码 bcrypt;冲突返回 409。
- Step 2: GET 列表
分页:limit/offset;仅返回 id, username, email, role, status, publicSlug, createdAt。
- Step 3: PATCH 禁用/启用
{ status: 'active' | 'disabled' };若禁用,删除该用户所有 sessions(SQLite delete from sessions where user_id = ?)。
- Step 4: 保护路由
这些路径 不要 allowlist;依赖全局 auth guard + requireAdmin。
- Step 5: Commit
git add server/api/admin
git commit -m "feat(admin): user provisioning and status endpoints"
Task 7: 可见性与 shareToken 工具 + 单元测试
Files:
-
Create:
server/constants/visibility.ts -
Create:
server/utils/share-token.ts -
Create:
server/utils/visibility.test.ts -
Step 1: 定义
VISIBILITY与 Zod
private | unlisted | public 三值。
- Step 2:
ensureShareToken(visibility, existing?: string | null)
unlisted 且无 existing → 生成;public/private → 返回 null(或保留策略:清 token)。
- Step 3: 测试
Run:
bun test server/utils/visibility.test.ts
- Step 4: Commit
git add server/constants/visibility.ts server/utils/share-token.ts server/utils/visibility.test.ts
git commit -m "test: visibility and share token helpers"
Task 8: RSS URL 校验 + 测试
Files:
-
Create:
server/utils/rss-url.ts -
Create:
server/utils/rss-url.test.ts -
Step 1: 实现
assertSafeHttpUrl(url: string) -
仅
http:/https: -
使用
new URL(url);hostname 非空 -
dns.lookup或先fetch前解析 IP(若环境限制,可先用isHostnameIpLiteral分支处理 IP 字面量禁止私网段) -
拒绝 obvious 内网 host(
localhost、.local可选) -
Step 2: 测试用例
允许 https://example.com/feed,拒绝 http://127.0.0.1/、http://192.168.1.1/。
- Step 3: Commit
git add server/utils/rss-url.ts server/utils/rss-url.test.ts
git commit -m "feat(rss): block obvious SSRF targets for feed URLs"
Task 9: Posts 服务与 me/public API
Files:
-
Create:
server/service/posts/index.ts -
Create:
server/api/me/posts.get.ts、me/posts.post.ts、me/posts/[id].get.ts、me/posts/[id].put.ts、me/posts/[id].delete.ts -
Create:
server/api/public/posts/[publicSlug].get.ts(或聚合在 profile API) -
Step 1: 服务层函数
listForUser、create、update、delete,均在 SQL 层带 userId。
- Step 2:
public列表
仅 visibility='public' 且用户 status=active;按 publishedAt 排序。
- Step 3: unlisted 不进入列表
由 Task 10 的专用路由处理。
- Step 4: Commit
git add server/service/posts server/api/me/posts server/api/public
git commit -m "feat(posts): CRUD and public listing"
Task 10: Timeline 与 APIs
Files:
-
Create:
server/service/timeline/index.ts -
Create:
server/api/me/timeline.*.ts -
扩展:
server/api/public/profile/[publicSlug].get.ts合并时间线 -
Step 1: CRUD 与可见性字段同步
更新 visibility 时调用 ensureShareToken。
- Step 2: Commit
git add server/service/timeline server/api/me/timeline
git commit -m "feat(timeline): CRUD with visibility"
Task 11: Profile me + 公开聚合 API
Files:
-
Create:
server/service/profile/index.ts -
Create:
server/api/me/profile.get.ts、me/profile.put.ts -
Create:
server/api/public/profile/[publicSlug].get.ts -
Step 1: 公开聚合返回结构
{
"user": { "publicSlug", "nickname", "avatar" },
"bio": { "markdown", "visibility" },
"links": [{ "label", "url", "visibility" }],
"posts": [ ...public only ],
"timeline": [ ...public only ],
"rssItems": [ ...public only ]
}
仅包含 visibility === 'public' 的块;bio/avatar 遵循各自 visibility。
- Step 2: disabled 用户
查询用户时若 status !== 'active' → 404。
- Step 3: Commit
git add server/service/profile server/api/me/profile server/api/public/profile
git commit -m "feat(profile): editable profile and public aggregate endpoint"
Task 12: RSS 订阅、抓取、去重、me API
Files:
-
Modify:
package.json(添加 XML 解析依赖,例如fast-xml-parser) -
Create:
server/service/rss/fetch.ts -
Create:
server/service/rss/parser.ts -
Create:
server/api/me/rss/feeds.*.ts、me/rss/items.*.ts、me/rss/sync.post.ts -
Step 1: 安装依赖
Run:
cd /home/dash/projects/person-panel && bun add fast-xml-parser
- Step 2: 抓取流程
对每个 feed:assertSafeHttpUrl → fetch(timeout + max bytes 读取截断)→ 解析 item → insert 忽略唯一冲突;新行 visibility: private。
- Step 3:
sync.post.ts
触发当前用户所有到期源或指定 feedId。
- Step 4: Commit
git add package.json server/service/rss server/api/me/rss
git commit -m "feat(rss): feeds, items, fetch, and manual sync"
Task 13: 进程内调度器
Files:
-
Create:
server/service/rss/scheduler.ts -
Create:
server/plugins/04.rss-scheduler.ts -
Step 1:
ready钩子
setInterval 每 RSS_SYNC_INTERVAL_MINUTES 扫描;setTimeout 启动 5s 后补跑。
- Step 2: 并发池
例如 p-limit 风格手写 semaphore:RSS_MAX_CONCURRENT_FEEDS。
- Step 3: Commit
git add server/service/rss/scheduler.ts server/plugins/04.rss-scheduler.ts
git commit -m "feat(rss): in-process polling scheduler"
Task 14: Unlisted 公开详情 API
Files:
-
Create:
server/api/public/unlisted/[publicSlug]/[shareToken].get.ts(路径按 Nitro 约定调整) -
Step 1: 查询顺序
依次尝试 posts、timeline_events、rss_items(可加 type 查询参数减少歧义;若 token 全局唯一概率足够可单次 union)。
推荐:token 全局唯一(随机 32 byte)→ 单表查询 where shareToken = ? AND user.publicSlug = ? join users。
- Step 2: 返回 404
用户 disabled 或 visibility !== 'unlisted'。
- Step 3: Commit
git add server/api/public/unlisted
git commit -m "feat(public): resolve unlisted share links"
Task 15: 前端路由与 auth 规则
Files:
-
Modify:
app/utils/auth-routes.ts -
Modify:
app/middleware/auth.global.ts(若需把/me纳入保护) -
Modify:
app/pages/login/index.vue(文案) -
Step 1:
PUBLIC_ROUTE_PREFIXES
加入 "/@"、"/p/"。
- Step 2: 登录后落地页
指向 /me 或 /dashboard(与侧栏一致)。
- Step 3: 注册页策略
若关闭自助注册:allowRegister 配置为 false(沿用 useGlobalConfig),register 路由 middleware 重定向登录或 404。
- Step 4: Commit
git add app/utils/auth-routes.ts app/middleware/auth.global.ts app/pages/login/index.vue
git commit -m "feat(app): public routes for profile and unlisted links"
Task 16: Nuxt UI — 公开页与后台壳
Files:
-
Create:
app/pages/@/[publicSlug]/index.vue -
Create:
app/pages/p/[publicSlug]/t/[shareToken].vue -
Create:
app/layouts/public.vue -
Modify:
app/components/AppShell.vue -
Create:
app/pages/me/index.vue、app/pages/me/posts/**等(按最终 IA) -
Step 1: 公开主页
useFetch('/api/public/profile/' + slug);UPage、UCard 列表;空状态 UEmpty。
- Step 2: 分享页
useFetch unlisted API;403/404 用 UAlert。
- Step 3:
AppShell侧栏
使用 UNavigationMenu;admin 显示「用户管理」。
- Step 4: Commit
git add app/pages app/layouts/public.vue app/components/AppShell.vue
git commit -m "feat(ui): public and authenticated shells with Nuxt UI"
Task 17: 集成验证与文档收尾
- Step 1: 手测脚本(记录在 PR 描述)
- 创建 admin + 普通用户;普通用户登录创建
publicSlug。 - 写文章
public,匿名访问/@slug可见。 - 写
unlisted,匿名访问/p/slug/t/token可见且不在列表。 - 添加 RSS,手动 sync,条目默认
private,改public后出现在公开页。 - 禁用用户,公开页 404。
- Step 2: 文档
如有实现偏差,更新 docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md 对应段落;全部验收通过后再将 spec 顶部状态改为「已实现」。
- Step 3: Commit(若有文档变更)
git add docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md
git commit -m "docs: sync spec with implementation"
Plan Self-Review(对照 spec)
| Spec 章节 | 覆盖任务 |
|---|---|
| §1–2 目标/模块 | Task 1–17 |
| §3 数据模型/可见性/token | Task 1,7,9–12,14 |
| §4 API 分层 | Task 4–6,9–14 |
| §5 管理员/bootstrap/注册 | Task 2,6,15 |
| §6 UI/§6.1 体验 | Task 15–16 |
| §7 定时任务 | Task 13 |
| §8 安全 | Task 5,8,12 |
| §9–11 错误/测试/配置 | Task 7–8 测试 + Task 17 |
占位符扫描: 无 TBD;具体页面路径在 Task 16 允许按实现微调但需保持单一路由风格。
类型一致性: MinimalUser 在 Task 3 增加 role;visibility 三态在 Task 7 单列常量。
Plan complete and saved to docs/superpowers/plans/2026-04-18-person-panel-multitenant-hub-implementation-plan.md. Two execution options:
- Subagent-Driven (recommended) — 每个 Task 派生子代理并在任务间复核,迭代快
- Inline Execution — 本会话用 executing-plans 批量执行并设检查点
你更倾向哪一种?