From 2dac8bc47b8d8e36c88e587ac3a4e9c16bc6a33b Mon Sep 17 00:00:00 2001 From: linmeng <819991304@qq.com> Date: Wed, 29 Jul 2026 10:14:39 +0800 Subject: [PATCH] fix: expose staging admin entry Use persisted self-hosted roles and the isolated admin origin so authorized staging accounts can discover the protected admin surface. --- docs/BUG_HISTORY.md | 16 +++++++++++ frontend/src/app/api/account/route.ts | 11 +++++++- frontend/src/app/page.tsx | 3 +++ frontend/src/components/app-sidebar.tsx | 5 ++-- frontend/src/lib/supabase/admin.ts | 18 ++++++++++++- frontend/tests/account-api.test.ts | 9 +++++++ frontend/tests/admin-contracts.test.ts | 15 +++++++++++ frontend/tests/admin-users-contract.test.ts | 30 ++++++++++----------- frontend/tests/sidebar-contract.test.ts | 12 +++++++++ 9 files changed, 100 insertions(+), 19 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 329fcca0..31188ccf 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -1578,3 +1578,19 @@ - 防复发:当前事件延续必须是服务端 opportunity 所有权规则,而不是 prompt 建议;模型输出即使结构合法,也必须经过 bounded tool budget、active-opportunity lookup、decision validation 和 completion payload hash 四层门控。 - 相关记录:BUG-075、BUG-085 - 修复版本:本地 V5 重构,待提交与 staging 验收 + +## BUG-087 | self-hosted staging 管理员看不到独立后台入口 + +- 状态:resolved +- 首次发现:2026-07-29 +- 最近更新:2026-07-29 +- 影响面:self-hosted staging 账户菜单、`GET /api/account`、独立后台入口;不影响后台独立登录与 `requireAdminSession` +- 用户现象:身份库已持久化 `admin` 或 `viewer` 角色的用户登录主站后,账户菜单不显示后台入口;即使显示旧入口,主站 `/admin` 路径也会返回 404。 +- 触发条件:`AUTH_PROVIDER=self-hosted`,后台部署在与主站不同的 `AUTH_ADMIN_ORIGIN`,用户角色以逗号分隔形式持久化在 `identity.users.role`。 +- 根因:主站 `isAdminUser` 对 self-hosted 模式直接返回 `false`,没有读取持久化角色;侧栏又把入口写死为主站相对路径 `/admin/codes`。既有后台鉴权已按持久化角色执行,但主站入口发现逻辑没有复用同一授权事实,独立域名部署合同也没有进入账户响应。 +- 修复:self-hosted 分支通过现有 `ADMIN_DATABASE_URL` 管理只读连接查询当前用户的 `identity.users.role`,仅 `admin` 或 `viewer` 可见入口,且不使用 `ADMIN_EMAILS` 替代角色授权;`GET /api/account` 在服务端解析身份配置并返回 `AUTH_ADMIN_ORIGIN + /admin/codes`,Supabase 模式继续返回 `/admin/codes`;账户与侧栏类型透传该 URL,并将文案改为“后台管理”。后台独立登录和 `requireAdminSession` 保持不变。 +- 验证:`frontend/tests/admin-contracts.test.ts`、`frontend/tests/admin-users-contract.test.ts`、`frontend/tests/account-api.test.ts`、`frontend/tests/sidebar-contract.test.ts` 锁定持久化角色、独立后台 URL、服务端环境边界和后台写权限门禁;目标 TypeScript、构建与 staging 登录态 smoke 结果另行记录。 +- 防复发:self-hosted 主站入口发现必须以 `identity.users.role` 为授权事实,不能退回邮箱 allowlist;客户端不得读取后台 origin 环境变量或硬编码主站 `/admin` 路径;后台 API 必须继续独立执行 `requireAdminSession`,入口可见性不得被当作授权。 +- 相关记录:BUG-010、BUG-083、BUG-084 +- 复发自:BUG-010 +- 修复版本:待提交(本地可测) diff --git a/frontend/src/app/api/account/route.ts b/frontend/src/app/api/account/route.ts index e151abcf..d3b594ee 100644 --- a/frontend/src/app/api/account/route.ts +++ b/frontend/src/app/api/account/route.ts @@ -13,6 +13,7 @@ import { isSupabaseConfigurationError, } from "@/lib/supabase/config"; import { createServerSupabaseClient } from "@/lib/supabase/server"; +import { readIdentityConfig } from "@/modules/identity/config"; export const runtime = "nodejs"; @@ -109,11 +110,19 @@ export async function GET() { profile, Array.isArray(rectificationCaseRows) ? rectificationCaseRows : [], ); + const isAdmin = await isAdminUser(user); + const identityConfig = readIdentityConfig(process.env); + const adminUrl = isAdmin + ? identityConfig.provider === "self-hosted" + ? new URL("/admin/codes", identityConfig.adminOrigin).toString() + : "/admin/codes" + : null; return NextResponse.json({ user: { id: user.id, email: user.email ?? null }, credits: profile.credits, - isAdmin: await isAdminUser(user), + isAdmin, + adminUrl, rectificationPriceCredits, hasConfirmedBirthTime: profile.birth_time_status === "confirmed" && typeof profile.active_birth_time === "string", diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 4b6fdac9..3ce7621f 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -180,6 +180,7 @@ type Account = { user: { id: string; email: string | null }; credits: number; isAdmin: boolean; + adminUrl: string | null; rectificationPriceCredits: number; hasConfirmedBirthTime: boolean; rectificationCase: AccountRectificationCaseState | null; @@ -1243,6 +1244,7 @@ export default function Home() { user: { id: "preview-user", email: "preview@local.test" }, credits: 8, isAdmin: false, + adminUrl: null, rectificationPriceCredits: 1, hasConfirmedBirthTime: previewProfile.birthTimeStatus === "confirmed", rectificationCase: null, @@ -2789,6 +2791,7 @@ export default function Home() { email: account.user.email || "尚未读取邮箱", credits: account.credits, isAdmin: account.isAdmin, + adminUrl: account.adminUrl, initial: profile.name.trim().slice(0, 1) || account.user.email?.slice(0, 1).toUpperCase() || "你", diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx index 6a4a2516..974f3d8b 100644 --- a/frontend/src/components/app-sidebar.tsx +++ b/frontend/src/components/app-sidebar.tsx @@ -38,6 +38,7 @@ export type SidebarAccount = { email: string; credits: number; isAdmin: boolean; + adminUrl: string | null; initial: string; }; @@ -214,8 +215,8 @@ export function AppSidebar({ - {account.isAdmin && onAccountMenuOpenChange(false)}> -