Use persisted self-hosted roles and the isolated admin origin so authorized staging accounts can discover the protected admin surface.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|| "你",
|
||||
|
||||
@@ -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({
|
||||
<button className="account-menu-item" role="menuitem" type="button" onClick={() => handleAccountAction(onOpenRedeem)}>
|
||||
<Gift aria-hidden="true" /><span>兑换点数</span><small>{account.credits} 点</small>
|
||||
</button>
|
||||
{account.isAdmin && <Link className="account-menu-item" href="/admin/codes" role="menuitem" onClick={() => onAccountMenuOpenChange(false)}>
|
||||
<KeyRound aria-hidden="true" /><span>管理兑换码</span><ChevronRight aria-hidden="true" />
|
||||
{account.isAdmin && account.adminUrl && <Link className="account-menu-item" href={account.adminUrl} role="menuitem" onClick={() => onAccountMenuOpenChange(false)}>
|
||||
<KeyRound aria-hidden="true" /><span>后台管理</span><ChevronRight aria-hidden="true" />
|
||||
</Link>}
|
||||
<div className="account-menu-separator" role="separator" />
|
||||
<button className="account-menu-item account-menu-danger" role="menuitem" type="button" onClick={() => handleAccountAction(onOpenLogout)}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "server-only";
|
||||
|
||||
import { queryAdminRows } from "@/lib/admin/database";
|
||||
import {
|
||||
createAdminSupabaseClient,
|
||||
isAdminEmail,
|
||||
@@ -8,8 +9,23 @@ import {
|
||||
export { createAdminSupabaseClient, isAdminEmail };
|
||||
|
||||
export async function isAdminUser(user: { id?: string; email?: string | null }) {
|
||||
if (process.env.AUTH_PROVIDER?.trim() === "self-hosted") {
|
||||
if (!user.id) return false;
|
||||
try {
|
||||
const rows = await queryAdminRows<{ role: string }>(
|
||||
"select role from identity.users where id = $1 limit 1",
|
||||
[user.id],
|
||||
);
|
||||
return rows[0]?.role
|
||||
.split(",")
|
||||
.map((role) => role.trim())
|
||||
.some((role) => role === "admin" || role === "viewer") ?? false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (isAdminEmail(user.email)) return true;
|
||||
if (process.env.AUTH_PROVIDER?.trim() === "self-hosted" || !user.id) return false;
|
||||
if (!user.id) return false;
|
||||
|
||||
try {
|
||||
const admin = createAdminSupabaseClient();
|
||||
|
||||
Reference in New Issue
Block a user