diff --git a/.dev.vars.example b/.dev.vars.example index dfef0ea..8baab46 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -1,5 +1,6 @@ KOC_PORTAL_URL=http://localhost:3000 -ADMIN_ALLOWED_EMAIL=operator@example.com +SUPER_ADMIN_USERNAME=admin +SUPER_ADMIN_PASSWORD=qazxsw123admin ADMIN_INTERNAL_TOKEN=replace-with-a-random-secret # Optional override. The production key must be stored as a runtime secret. diff --git a/README.md b/README.md index 872edc1..0f3499f 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,14 @@ npm run build 飞书自建应用需开通电子表格读取、知识库节点读取和云文档素材下载权限, 并将应用添加到目标知识库或电子表格的文档应用中。 +后台登录首次启动还需要配置: + +- `SUPER_ADMIN_USERNAME`:唯一的超级管理员登录账号 +- `SUPER_ADMIN_PASSWORD`:超级管理员初始密码,至少 8 位 +- `ADMIN_INTERNAL_TOKEN`:自动采集等内部任务使用的服务密钥 + +系统首次登录时创建唯一的超级管理员。后续管理员和普通用户均由“用户管理”页面创建,普通用户不能访问 KOC 资源库。 + This starter does not use `wrangler.jsonc`. ## Included Shape @@ -33,63 +41,13 @@ This starter does not use `wrangler.jsonc`. - `examples/d1/` contains an optional D1 example surface - `drizzle.config.ts` supports local migration generation when needed -## Workspace Auth Headers +## 后台账号与角色 -OpenAI workspace sites can read the current user's email from -`oai-authenticated-user-email`. +- 超级管理员:唯一系统管理员,可管理管理员和普通用户。 +- 管理员:可访问全部业务模块,可创建和重置普通用户账号。 +- 普通用户:可使用工作台、任务、内容分发和数据回收,不可查看或导出 KOC 资源库。 -SIWC-authenticated workspace sites may also receive -`oai-authenticated-user-full-name` when the user's SIWC profile has a non-empty -`name` claim. The full-name value is percent-encoded UTF-8 and is accompanied by -`oai-authenticated-user-full-name-encoding: percent-encoded-utf-8`. - -Treat the full name as optional and fall back to email when it is absent: - -```tsx -import { headers } from "next/headers"; - -export default async function Home() { - const requestHeaders = await headers(); - const email = requestHeaders.get("oai-authenticated-user-email"); - const encodedFullName = requestHeaders.get("oai-authenticated-user-full-name"); - const fullName = - encodedFullName && - requestHeaders.get("oai-authenticated-user-full-name-encoding") === - "percent-encoded-utf-8" - ? decodeURIComponent(encodedFullName) - : null; - - const displayName = fullName ?? email; - // ... -} -``` - -## Optional Dispatch-Owned ChatGPT Sign-In - -Import the ready-to-use helpers from `app/chatgpt-auth.ts` when the site needs -optional or required ChatGPT sign-in: - -- Use `getChatGPTUser()` for optional signed-in UI. -- Use `requireChatGPTUser(returnTo)` for server-rendered pages that should send - anonymous visitors through Sign in with ChatGPT. -- Use `chatGPTSignInPath(returnTo)` and `chatGPTSignOutPath(returnTo)` for - browser links or actions. -- Pass a same-origin relative `returnTo` path for the destination after sign-in - or sign-out. The helper validates and safely encodes it. -- Mark protected pages with `export const dynamic = "force-dynamic"` because - they depend on per-request identity headers. - -Dispatch owns `/signin-with-chatgpt`, `/signout-with-chatgpt`, `/callback`, the -OAuth cookies, and identity header injection. Do not implement app routes for -those reserved paths. Routes that do not import and call the helper remain -anonymous-compatible. - -SIWC establishes identity only; it does not prove workspace membership. Use the -Sites hosting platform's access policy controls for workspace-wide restrictions, -or enforce explicit server-side membership or allowlist checks. - -Use SIWC for account pages, user-specific dashboards, saved records, and write -actions tied to the current ChatGPT user. Leave public content anonymous. +后台不提供注册、找回密码和普通用户个人改密。密码重置统一由管理员在后台完成。 ## Useful Commands diff --git a/app/admin-app.tsx b/app/admin-app.tsx index 7443d14..0b1fb5a 100644 --- a/app/admin-app.tsx +++ b/app/admin-app.tsx @@ -12,6 +12,8 @@ import { formatShanghaiDate as formatDate, formatShanghaiToday, } from "../lib/date-utils"; +import type { AuthUser } from "../lib/user-auth"; +import UsersPage from "./users-page"; type Partner = { id: string; @@ -122,7 +124,8 @@ type NavKey = | "tasks" | "distributions" | "resources" - | "recovery"; + | "recovery" + | "users"; const NAV_ITEMS: Array<{ key: NavKey; label: string; mark: string }> = [ { key: "overview", label: "工作台", mark: "⌂" }, @@ -130,6 +133,7 @@ const NAV_ITEMS: Array<{ key: NavKey; label: string; mark: string }> = [ { key: "distributions", label: "内容分发", mark: "↗" }, { key: "resources", label: "KOC资源", mark: "◎" }, { key: "recovery", label: "数据回收", mark: "◫" }, + { key: "users", label: "用户管理", mark: "♙" }, ]; const EMPTY_DATA: DashboardData = { @@ -301,7 +305,7 @@ async function compressScreenshot(file: File) { }); } -export default function Home() { +export default function Home({ currentUser }: { currentUser: AuthUser }) { const [data, setData] = useState(EMPTY_DATA); const [activeNav, setActiveNav] = useState("overview"); const [loading, setLoading] = useState(true); @@ -322,6 +326,12 @@ export default function Home() { const [metricForm, setMetricForm] = useState({ exposure: "", views: "" }); const [exportingTaskId, setExportingTaskId] = useState(null); const [exportingResources, setExportingResources] = useState(false); + const isManager = currentUser.role === "super_admin" || currentUser.role === "admin"; + const visibleNavItems = NAV_ITEMS.filter((item) => { + if (item.key === "resources" && currentUser.role === "user") return false; + if (item.key === "users" && !isManager) return false; + return true; + }); const loadData = useCallback(async () => { try { @@ -399,6 +409,8 @@ export default function Home() { const recentDistributions = data.distributions.slice(0, 6); const navigate = (key: NavKey) => { + if (key === "resources" && currentUser.role === "user") return; + if (key === "users" && !isManager) return; setActiveNav(key); setMenuOpen(false); }; @@ -646,6 +658,15 @@ export default function Home() { title: "数据回收", subtitle: "按任务设置自动采集日,集中查看最新公开数据与创作者截图。", }, + users: { + title: "用户管理", + subtitle: "由后台统一创建账号和重置密码,不开放自助注册与个人改密。", + }, + }; + + const logout = async () => { + await fetch("/api/auth/logout", { method: "POST" }).catch(() => null); + window.location.assign("/login"); }; return ( @@ -661,7 +682,7 @@ export default function Home() {