feat: add agent admin settings access
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-17 23:16:52 +08:00
parent 57b784f542
commit ea5a189ae0
15 changed files with 104 additions and 28 deletions

View File

@ -13,6 +13,12 @@ export default async function AdminLayout({
getNavLinks(), getNavLinks(),
getTutorialVideos(), getTutorialVideos(),
]); ]);
const settingsHref =
session?.user?.type === "admin"
? "/settings"
: session?.user?.type === "agent_admin"
? "/settings/agent-workflow"
: null;
return ( return (
<div className="flex min-h-screen flex-col bg-background"> <div className="flex min-h-screen flex-col bg-background">
@ -20,7 +26,8 @@ export default async function AdminLayout({
siteName={siteSettings.siteName} siteName={siteSettings.siteName}
siteLogo={siteSettings.siteLogo || undefined} siteLogo={siteSettings.siteLogo || undefined}
siteLogoDark={siteSettings.siteLogoDark || undefined} siteLogoDark={siteSettings.siteLogoDark || undefined}
isAdmin={session?.user?.type === "admin"} isAdmin={session?.user?.type === "admin" || session?.user?.type === "agent_admin"}
settingsHref={settingsHref}
customLinks={navLinks.map((link) => ({ customLinks={navLinks.map((link) => ({
id: link.id, id: link.id,
name: link.name, name: link.name,
@ -47,4 +54,3 @@ export default async function AdminLayout({
</div> </div>
); );
} }

View File

@ -128,7 +128,7 @@ type User = {
email: string; email: string;
name: string | null; name: string | null;
department: string | null; department: string | null;
role: "guest" | "regular" | "admin"; role: "guest" | "regular" | "admin" | "agent_admin";
isHighVip: boolean; isHighVip: boolean;
isActive: boolean; isActive: boolean;
monthlyImageLimit: number | null; monthlyImageLimit: number | null;
@ -141,6 +141,7 @@ const ROLE_LABELS: Record<User["role"], string> = {
guest: "访客用户", guest: "访客用户",
regular: "普通用户", regular: "普通用户",
admin: "管理员", admin: "管理员",
agent_admin: "智能体管理员",
}; };
type ToolConfig = { type ToolConfig = {
@ -3172,6 +3173,7 @@ export function AdminSettingsClient() {
<SelectItem value="guest">访客</SelectItem> <SelectItem value="guest">访客</SelectItem>
<SelectItem value="regular">普通</SelectItem> <SelectItem value="regular">普通</SelectItem>
<SelectItem value="admin">管理员</SelectItem> <SelectItem value="admin">管理员</SelectItem>
<SelectItem value="agent_admin">智能体管理员</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Button <Button

View File

@ -119,7 +119,15 @@ function createWorkflowStepDraft(
}; };
} }
export function AgentWorkflowSettingsClient() { interface AgentWorkflowSettingsClientProps {
backHref?: string;
backLabel?: string;
}
export function AgentWorkflowSettingsClient({
backHref = "/settings",
backLabel = "返回设置",
}: AgentWorkflowSettingsClientProps = {}) {
const { data, isLoading, mutate } = useSWR<{ const { data, isLoading, mutate } = useSWR<{
agentWorkflowUiConfig?: AgentWorkflowUiConfig; agentWorkflowUiConfig?: AgentWorkflowUiConfig;
agentWorkflowMaterialApiConfig?: AgentWorkflowMaterialApiConfig; agentWorkflowMaterialApiConfig?: AgentWorkflowMaterialApiConfig;
@ -403,9 +411,9 @@ export function AgentWorkflowSettingsClient() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button asChild variant="outline"> <Button asChild variant="outline">
<Link href="/settings"> <Link href={backHref}>
<ArrowLeft className="mr-2 h-4 w-4" /> <ArrowLeft className="mr-2 h-4 w-4" />
返回设置 {backLabel}
</Link> </Link>
</Button> </Button>
<Button onClick={handleSave} disabled={isSaving}> <Button onClick={handleSave} disabled={isSaving}>

View File

@ -1,15 +1,19 @@
import { requireAdmin } from "@/lib/middleware/admin"; import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { AgentWorkflowSettingsClient } from "./agent-workflow-settings-client"; import { AgentWorkflowSettingsClient } from "./agent-workflow-settings-client";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export default async function AgentWorkflowSettingsPage() { export default async function AgentWorkflowSettingsPage() {
try { const session = await requireAgentWorkflowAdmin().catch(() => null);
await requireAdmin(); if (!session) {
} catch {
redirect("/"); redirect("/");
} }
return <AgentWorkflowSettingsClient />; return (
<AgentWorkflowSettingsClient
backHref={session.user.type === "admin" ? "/settings" : "/"}
backLabel={session.user.type === "admin" ? "返回设置" : "返回首页"}
/>
);
} }

View File

@ -1,3 +1,4 @@
import { auth } from "@/app/(auth)/auth";
import { requireAdmin } from "@/lib/middleware/admin"; import { requireAdmin } from "@/lib/middleware/admin";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { AdminSettingsClient } from "./admin-settings-client"; import { AdminSettingsClient } from "./admin-settings-client";
@ -6,6 +7,11 @@ import { AdminSettingsClient } from "./admin-settings-client";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export default async function AdminSettingsPage() { export default async function AdminSettingsPage() {
const session = await auth().catch(() => null);
if (session?.user?.type === "agent_admin") {
redirect("/settings/agent-workflow");
}
try { try {
await requireAdmin(); await requireAdmin();
} catch { } catch {
@ -14,4 +20,3 @@ export default async function AdminSettingsPage() {
return <AdminSettingsClient />; return <AdminSettingsClient />;
} }

View File

@ -11,7 +11,7 @@ import { db } from "@/lib/db";
import { loginToken, user as userTable } from "@/lib/db/schema"; import { loginToken, user as userTable } from "@/lib/db/schema";
import { eq, and, gt } from "drizzle-orm"; import { eq, and, gt } from "drizzle-orm";
export type UserType = "guest" | "regular" | "admin"; export type UserType = "guest" | "regular" | "admin" | "agent_admin";
declare module "next-auth" { declare module "next-auth" {
interface Session extends DefaultSession { interface Session extends DefaultSession {

View File

@ -40,6 +40,12 @@ async function SidebarWrapper({ children }: { children: React.ReactNode }) {
const isCollapsed = cookieStore.get("sidebar_state")?.value !== "true"; const isCollapsed = cookieStore.get("sidebar_state")?.value !== "true";
const homeVariant = const homeVariant =
homeVariantSetting?.value === "lovart" ? "lovart" : "classic"; homeVariantSetting?.value === "lovart" ? "lovart" : "classic";
const settingsHref =
session?.user?.type === "admin"
? "/settings"
: session?.user?.type === "agent_admin"
? "/settings/agent-workflow"
: null;
return ( return (
<ChatShell <ChatShell
@ -49,7 +55,8 @@ async function SidebarWrapper({ children }: { children: React.ReactNode }) {
siteName={siteSettings.siteName} siteName={siteSettings.siteName}
siteLogo={siteSettings.siteLogo || undefined} siteLogo={siteSettings.siteLogo || undefined}
siteLogoDark={siteSettings.siteLogoDark || undefined} siteLogoDark={siteSettings.siteLogoDark || undefined}
isAdmin={session?.user?.type === "admin"} isAdmin={session?.user?.type === "admin" || session?.user?.type === "agent_admin"}
settingsHref={settingsHref}
customLinks={navLinks.map((link) => ({ customLinks={navLinks.map((link) => ({
id: link.id, id: link.id,
name: link.name, name: link.name,

View File

@ -10,7 +10,7 @@ import { ChatSDKError } from "@/lib/errors";
const updateUserSchema = z.object({ const updateUserSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
role: z.enum(["guest", "regular", "admin"]).optional(), role: z.enum(["guest", "regular", "admin", "agent_admin"]).optional(),
isHighVip: z.boolean().optional(), isHighVip: z.boolean().optional(),
isActive: z.boolean().optional(), isActive: z.boolean().optional(),
email: z.string().email().optional(), email: z.string().email().optional(),

View File

@ -4,6 +4,7 @@ import { db } from "@/lib/db";
import { siteSettings, systemSettings } from "@/lib/db/schema"; import { siteSettings, systemSettings } from "@/lib/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { auth } from "@/app/(auth)/auth"; import { auth } from "@/app/(auth)/auth";
import { isAgentWorkflowAdminType, isSiteAdminType } from "@/lib/middleware/admin";
import { import {
defaultMembershipConfig, defaultMembershipConfig,
defaultMembershipUserOverrides, defaultMembershipUserOverrides,
@ -209,7 +210,8 @@ function normalizeAgentCategoryTreeConfig(value: unknown): AgentCategoryTreeConf
export async function GET() { export async function GET() {
try { try {
const session = await auth().catch(() => null); const session = await auth().catch(() => null);
const isAdmin = session?.user?.type === "admin"; const isAdmin = isSiteAdminType(session?.user?.type);
const canManageAgentWorkflow = isAgentWorkflowAdminType(session?.user?.type);
const [ const [
settings, settings,
notificationSetting, notificationSetting,
@ -405,7 +407,7 @@ export async function GET() {
const agentWorkflowUiConfig = normalizeAgentWorkflowUiConfig( const agentWorkflowUiConfig = normalizeAgentWorkflowUiConfig(
agentWorkflowUiConfigSetting?.value agentWorkflowUiConfigSetting?.value
); );
const agentWorkflowMaterialApiConfig = isAdmin const agentWorkflowMaterialApiConfig = canManageAgentWorkflow
? normalizeAgentWorkflowMaterialApiConfig(agentWorkflowMaterialApiConfigSetting?.value) ? normalizeAgentWorkflowMaterialApiConfig(agentWorkflowMaterialApiConfigSetting?.value)
: normalizeAgentWorkflowMaterialApiConfigForPublic( : normalizeAgentWorkflowMaterialApiConfigForPublic(
agentWorkflowMaterialApiConfigSetting?.value agentWorkflowMaterialApiConfigSetting?.value
@ -669,7 +671,7 @@ const updateSchema = z.object({
.optional(), .optional(),
}); });
// PUT /api/site-settings - 更新网站设置(仅管理员) // PUT /api/site-settings - 更新网站设置(管理员 / 智能体管理员)
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
try { try {
const session = await auth(); const session = await auth();
@ -678,11 +680,25 @@ export async function PUT(request: NextRequest) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
} }
if (session.user.type !== "admin") { const isAdmin = isSiteAdminType(session.user.type);
const canManageAgentWorkflow = isAgentWorkflowAdminType(session.user.type);
if (!canManageAgentWorkflow) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 }); return NextResponse.json({ error: "Forbidden" }, { status: 403 });
} }
const body = await request.json(); const body = await request.json();
if (!isAdmin) {
const allowedKeys = new Set([
"agentWorkflowUiConfig",
"agentWorkflowMaterialApiConfig",
"agentWorkflowBrandModelConfig",
]);
const hasRestrictedKeys = Object.keys(body || {}).some((key) => !allowedKeys.has(key));
if (hasRestrictedKeys) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
}
const data = updateSchema.parse(body); const data = updateSchema.parse(body);
const { const {
homeVariant, homeVariant,

View File

@ -32,6 +32,7 @@ type ChatShellProps = {
siteLogo?: string; siteLogo?: string;
siteLogoDark?: string; siteLogoDark?: string;
isAdmin: boolean; isAdmin: boolean;
settingsHref?: string | null;
customLinks: CustomLink[]; customLinks: CustomLink[];
toolbox1Name: string; toolbox1Name: string;
toolbox1Enabled: boolean; toolbox1Enabled: boolean;
@ -52,6 +53,7 @@ export function ChatShell({
siteLogo, siteLogo,
siteLogoDark, siteLogoDark,
isAdmin, isAdmin,
settingsHref = null,
customLinks, customLinks,
toolbox1Name, toolbox1Name,
toolbox1Enabled, toolbox1Enabled,
@ -76,6 +78,7 @@ export function ChatShell({
siteLogo={siteLogo} siteLogo={siteLogo}
siteLogoDark={siteLogoDark} siteLogoDark={siteLogoDark}
isAdmin={isAdmin} isAdmin={isAdmin}
settingsHref={settingsHref}
customLinks={customLinks} customLinks={customLinks}
toolbox1Name={toolbox1Name} toolbox1Name={toolbox1Name}
toolbox1Enabled={toolbox1Enabled} toolbox1Enabled={toolbox1Enabled}

View File

@ -1599,13 +1599,15 @@ export function LovartHome({
</HoverCardContent> </HoverCardContent>
</HoverCard> </HoverCard>
) : null} ) : null}
{user?.type === "admin" ? ( {user?.type === "admin" || user?.type === "agent_admin" ? (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
type="button" type="button"
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5" className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5"
onClick={() => router.push("/settings")} onClick={() =>
router.push(user?.type === "agent_admin" ? "/settings/agent-workflow" : "/settings")
}
title="系统设置" title="系统设置"
> >
<Settings className="h-[18px] w-[18px]" /> <Settings className="h-[18px] w-[18px]" />

View File

@ -48,6 +48,7 @@ type TopNavbarProps = {
siteLogo?: string; siteLogo?: string;
siteLogoDark?: string; siteLogoDark?: string;
isAdmin?: boolean; isAdmin?: boolean;
settingsHref?: string | null;
customLinks?: CustomLink[]; customLinks?: CustomLink[];
toolbox1Name?: string; toolbox1Name?: string;
toolbox1Enabled?: boolean; toolbox1Enabled?: boolean;
@ -64,6 +65,7 @@ export function TopNavbar({
siteLogo, siteLogo,
siteLogoDark, siteLogoDark,
isAdmin = false, isAdmin = false,
settingsHref = null,
customLinks = [], customLinks = [],
toolbox1Name = "工具箱", toolbox1Name = "工具箱",
toolbox1Enabled = true, toolbox1Enabled = true,
@ -490,16 +492,16 @@ export function TopNavbar({
</a> </a>
</div> </div>
{/* 系统设置(仅管理员可见)*/} {/* 系统设置(管理员 / 智能体管理员可见)*/}
{isAdmin && ( {(isAdmin || settingsHref) && (
<div className="pb-4"> <div className="pb-4">
<a <a
href="/settings" href={settingsHref || "/settings"}
className={navItemBaseClass} className={navItemBaseClass}
title="系统设置" title="系统设置"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
window.location.href = "/settings"; window.location.href = settingsHref || "/settings";
}} }}
> >
<Settings className="h-5 w-5" /> <Settings className="h-5 w-5" />

View File

@ -1587,7 +1587,7 @@ export async function updateUser({
}: { }: {
id: string; id: string;
email?: string; email?: string;
role?: "guest" | "regular" | "admin"; role?: "guest" | "regular" | "admin" | "agent_admin";
isHighVip?: boolean; isHighVip?: boolean;
isActive?: boolean; isActive?: boolean;
monthlyImageLimit?: number | null; monthlyImageLimit?: number | null;

View File

@ -21,7 +21,7 @@ export const user = pgTable("User", {
name: varchar("name", { length: 100 }), // 用户姓名(用于显示) name: varchar("name", { length: 100 }), // 用户姓名(用于显示)
department: varchar("department", { length: 256 }), // 部门信息 department: varchar("department", { length: 256 }), // 部门信息
dingtalkUserId: varchar("dingtalkUserId", { length: 128 }), // 钉钉用户ID dingtalkUserId: varchar("dingtalkUserId", { length: 128 }), // 钉钉用户ID
role: varchar("role", { enum: ["guest", "regular", "admin"] }) role: varchar("role", { enum: ["guest", "regular", "admin", "agent_admin"] })
.notNull() .notNull()
.default("regular"), .default("regular"),
isHighVip: boolean("isHighVip").notNull().default(false), isHighVip: boolean("isHighVip").notNull().default(false),

View File

@ -1,6 +1,14 @@
import { auth } from "@/app/(auth)/auth"; import { auth } from "@/app/(auth)/auth";
import { ChatSDKError } from "@/lib/errors"; import { ChatSDKError } from "@/lib/errors";
export function isSiteAdminType(type?: string | null) {
return type === "admin";
}
export function isAgentWorkflowAdminType(type?: string | null) {
return type === "admin" || type === "agent_admin";
}
export async function requireAdmin() { export async function requireAdmin() {
const session = await auth(); const session = await auth();
@ -8,10 +16,23 @@ export async function requireAdmin() {
throw new ChatSDKError("unauthorized:admin"); throw new ChatSDKError("unauthorized:admin");
} }
if (session.user.type !== "admin") { if (!isSiteAdminType(session.user.type)) {
throw new ChatSDKError("forbidden:admin"); throw new ChatSDKError("forbidden:admin");
} }
return session; return session;
} }
export async function requireAgentWorkflowAdmin() {
const session = await auth();
if (!session?.user) {
throw new ChatSDKError("unauthorized:admin");
}
if (!isAgentWorkflowAdminType(session.user.type)) {
throw new ChatSDKError("forbidden:admin");
}
return session;
}