You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

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 扩展 usersrolestatuspublicSlugbioMarkdownbioVisibilitysocialLinksJsonavatarVisibility
packages/drizzle-pkg/database/sqlite/schema/content.ts(新建) poststimeline_events 表定义
packages/drizzle-pkg/database/sqlite/schema/rss.ts(新建) rss_feedsrss_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.statusactive 以外返回 nullMinimalUser 扩展 role
server/service/auth/context.ts MinimalUser 类型扩展
server/api/auth/me.get.ts 返回 rolepublicSlug 等安全字段
server/api/admin/users.post.ts 管理员创建/列表/禁用
server/api/me/posts.*.ts 文章 CRUD
server/api/me/timeline.*.ts 时间线 CRUD
server/api/me/profile.put.tsme/profile.get.ts 资料读写
server/api/me/rss/feeds.*.tsme/rss/items.*.tsme/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.tsrss.ts 导出

  • Step 1: 扩展 users

auth.tsusers 表增加(类型与默认值与迁移一致):

  • roletext,默认 'user',非空

  • statustext,默认 'active',非空

  • publicSlugtext,唯一索引(nullable 允许首登后填写)

  • bioMarkdowntext,可选

  • bioVisibilitytext,默认 'private'

  • socialLinksJsontext,默认 '[]'(存 JSON 字符串)

  • avatarVisibilitytext,默认 'private'

  • Step 2: 定义 posts

字段建议:iduserIdtitleslugbodyMarkdownexcerptcoverUrltagsJsonpublishedAtvisibilityshareTokencreatedAtupdatedAt;唯一索引 (userId, slug)

  • Step 3: 定义 timeline_events

字段:iduserIdoccurredOn(timestamp)、titlebodyMarkdownlinkUrlvisibilityshareTokencreatedAtupdatedAt

  • Step 4: 定义 rss_feeds / rss_items

rss_feedsiduserIdfeedUrl(唯一 per user)、titlesiteUrllastFetchedAtlastErrorpollIntervalMinutes 默认 null(用全局配置)、createdAt

rss_itemsiduserIdfeedIdguidcanonicalUrltitlesummarycontentSnippetauthorpublishedAtvisibility 默认 privateshareTokencreatedAt;索引 (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

逻辑:

  1. 查询是否存在 users.role = 'admin'
  2. 若不存在,读取 process.env.BOOTSTRAP_ADMIN_USERNAMEBOOTSTRAP_ADMIN_PASSWORD;缺一则 跳过并 log warn(避免误创建)。
  3. 使用与线上一致的 bcrypt hash 流程插入管理员: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.roleusers.statuswhere 保持 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 固定 userstatus 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.tsme/posts.post.tsme/posts/[id].get.tsme/posts/[id].put.tsme/posts/[id].delete.ts

  • Create: server/api/public/posts/[publicSlug].get.ts(或聚合在 profile API)

  • Step 1: 服务层函数

listForUsercreateupdatedelete,均在 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.tsme/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.*.tsme/rss/items.*.tsme/rss/sync.post.ts

  • Step 1: 安装依赖

Run:

cd /home/dash/projects/person-panel && bun add fast-xml-parser
  • Step 2: 抓取流程

对每个 feed:assertSafeHttpUrlfetch(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 钩子

setIntervalRSS_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: 查询顺序

依次尝试 poststimeline_eventsrss_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.vueapp/pages/me/posts/** 等(按最终 IA)

  • Step 1: 公开主页

useFetch('/api/public/profile/' + slug)UPageUCard 列表;空状态 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 描述)
  1. 创建 admin + 普通用户;普通用户登录创建 publicSlug
  2. 写文章 public,匿名访问 /@slug 可见。
  3. unlisted,匿名访问 /p/slug/t/token 可见且不在列表。
  4. 添加 RSS,手动 sync,条目默认 private,改 public 后出现在公开页。
  5. 禁用用户,公开页 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 增加 rolevisibility 三态在 Task 7 单列常量。


Plan complete and saved to docs/superpowers/plans/2026-04-18-person-panel-multitenant-hub-implementation-plan.md. Two execution options:

  1. Subagent-Driven (recommended) — 每个 Task 派生子代理并在任务间复核,迭代快
  2. Inline Execution — 本会话用 executing-plans 批量执行并设检查点

你更倾向哪一种?