refactor(modules): update classes, course-plans, diagnostic, questions, settings, student, layout
- Update classes data-access (invitations, main) for invitation management - Update course-plans actions, data-access, and types - Update diagnostic data-access for report queries - Update questions data-access for question bank queries - Update settings actions, ai-provider-settings-card, data-access, and types - Update student course-filters, student-courses-view, student-schedule-filters, student-schedule-view - Update layout app-sidebar, site-header, and navigation config
This commit is contained in:
@@ -5,7 +5,11 @@ import { revalidatePath } from "next/cache"
|
||||
import { createId } from "@paralleldrive/cuid2"
|
||||
|
||||
import type { ActionState } from "@/shared/types/action-state"
|
||||
import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard"
|
||||
import {
|
||||
requirePermission,
|
||||
PermissionDeniedError,
|
||||
getAuthContext,
|
||||
} from "@/shared/lib/auth-guard"
|
||||
import { Permissions } from "@/shared/types/permissions"
|
||||
import { encryptAiApiKey, getAiErrorMessage, testAiProviderById, testAiProviderConfig } from "@/shared/lib/ai"
|
||||
|
||||
@@ -15,13 +19,15 @@ import {
|
||||
deleteAiProvider as deleteAiProviderRecord,
|
||||
getAiProviderForUpdate,
|
||||
getAiProviderSummaries as fetchAiProviderSummaries,
|
||||
getAiProviderSummariesForUser,
|
||||
updateAiProvider,
|
||||
} from "./data-access"
|
||||
import type { AiProviderSummary } from "./types"
|
||||
import type { AiProviderSummary, AiProviderVisibility } from "./types"
|
||||
|
||||
export type { AiProviderSummary } from "./types"
|
||||
|
||||
const ProviderSchema = z.enum(["zhipu", "openai", "gemini", "custom"])
|
||||
const VisibilitySchema = z.enum(["public", "private"])
|
||||
|
||||
const AiProviderFormSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
@@ -30,6 +36,7 @@ const AiProviderFormSchema = z.object({
|
||||
model: z.string().min(1),
|
||||
apiKey: z.string().min(1).optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
visibility: VisibilitySchema.optional(),
|
||||
})
|
||||
|
||||
const AiProviderTestSchema = AiProviderFormSchema.extend({
|
||||
@@ -44,9 +51,15 @@ const AiProviderTestSchema = AiProviderFormSchema.extend({
|
||||
}
|
||||
})
|
||||
|
||||
const ensureUser = async (): Promise<{ id: string }> => {
|
||||
const ctx = await requirePermission(Permissions.AI_CONFIGURE)
|
||||
return { id: ctx.userId }
|
||||
/**
|
||||
* 校验当前用户身份,返回 { id, isAdmin }
|
||||
*
|
||||
* - 所有 AI_CHAT 用户均可访问(用于管理自己的 private provider)
|
||||
* - isAdmin 标识是否拥有 AI_CONFIGURE 权限(可管理 public provider 与他人 private)
|
||||
*/
|
||||
const ensureUser = async (): Promise<{ id: string; isAdmin: boolean }> => {
|
||||
const ctx = await requirePermission(Permissions.AI_CHAT)
|
||||
return { id: ctx.userId, isAdmin: ctx.permissions.includes(Permissions.AI_CONFIGURE) }
|
||||
}
|
||||
|
||||
const normalizeBaseUrl = (value: string | undefined): string | null => {
|
||||
@@ -58,10 +71,18 @@ const normalizeBaseUrl = (value: string | undefined): string | null => {
|
||||
.replace(/\/chat\/completions$/i, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户可见的 AI Provider 列表
|
||||
*
|
||||
* - 管理员:返回所有 public + private 记录
|
||||
* - 普通用户:返回 public + 自己创建的 private 记录
|
||||
*/
|
||||
export async function getAiProviderSummaries(): Promise<ActionState<AiProviderSummary[]>> {
|
||||
try {
|
||||
await ensureUser()
|
||||
const data = await fetchAiProviderSummaries()
|
||||
const user = await ensureUser()
|
||||
const data = user.isAdmin
|
||||
? await fetchAiProviderSummaries()
|
||||
: await getAiProviderSummariesForUser(user.id)
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
if (error instanceof PermissionDeniedError) return { success: false, message: error.message }
|
||||
@@ -85,10 +106,17 @@ export async function upsertAiProviderAction(
|
||||
return { success: false, message: "Base URL is required for this provider" }
|
||||
}
|
||||
|
||||
// 可见性规则:
|
||||
// - 管理员可创建/更新 public 或 private
|
||||
// - 普通用户只能创建/更新 private
|
||||
const requestedVisibility: AiProviderVisibility = payload.visibility ?? "private"
|
||||
const visibility: AiProviderVisibility =
|
||||
user.isAdmin ? requestedVisibility : "private"
|
||||
|
||||
// Parallelize default-count and existing-provider queries
|
||||
const [defaultCount, existing] = await Promise.all([
|
||||
countDefaultAiProviders(),
|
||||
payload.id ? getAiProviderForUpdate(payload.id) : Promise.resolve(null),
|
||||
payload.id ? getAiProviderForUpdate(payload.id, user.id) : Promise.resolve(null),
|
||||
])
|
||||
const hasDefault = defaultCount > 0
|
||||
|
||||
@@ -114,11 +142,13 @@ export async function upsertAiProviderAction(
|
||||
apiKeyEncrypted: encrypted,
|
||||
apiKeyLast4: last4,
|
||||
isDefault: isNextDefault,
|
||||
visibility,
|
||||
updatedBy: user.id,
|
||||
},
|
||||
payload.isDefault === true
|
||||
)
|
||||
|
||||
revalidatePath("/admin/ai-settings")
|
||||
revalidatePath("/settings")
|
||||
return { success: true, message: "AI provider updated", data: id }
|
||||
}
|
||||
@@ -141,16 +171,19 @@ export async function upsertAiProviderAction(
|
||||
apiKeyEncrypted: encrypted,
|
||||
apiKeyLast4: last4,
|
||||
isDefault: shouldMakeDefault,
|
||||
visibility,
|
||||
createdBy: user.id,
|
||||
updatedBy: user.id,
|
||||
},
|
||||
shouldMakeDefault
|
||||
)
|
||||
|
||||
revalidatePath("/admin/ai-settings")
|
||||
revalidatePath("/settings")
|
||||
return { success: true, message: "AI provider created", data: id }
|
||||
} catch (error) {
|
||||
if (error instanceof PermissionDeniedError) return { success: false, message: error.message }
|
||||
console.error("[upsertAiProviderAction] Failed to save AI provider:", error)
|
||||
return { success: false, message: "Failed to save AI provider" }
|
||||
}
|
||||
}
|
||||
@@ -190,18 +223,26 @@ const DeleteAiProviderSchema = z.object({
|
||||
/**
|
||||
* 删除 AI Provider
|
||||
*
|
||||
* 权限规则:
|
||||
* - 管理员(AI_CONFIGURE):可删除任意 Provider
|
||||
* - 普通用户(AI_CHAT):仅可删除自己创建的 Provider
|
||||
*
|
||||
* 如果删除的是默认 Provider,自动将最新的一条记录设为默认(若存在)。
|
||||
*/
|
||||
export async function deleteAiProviderAction(
|
||||
input: z.infer<typeof DeleteAiProviderSchema>
|
||||
): Promise<ActionState<null>> {
|
||||
try {
|
||||
await ensureUser()
|
||||
const user = await ensureUser()
|
||||
const parsed = DeleteAiProviderSchema.safeParse(input)
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: "Invalid provider id" }
|
||||
}
|
||||
await deleteAiProviderRecord(parsed.data.id)
|
||||
// 管理员不传 userId(可删除任意);普通用户传 userId 做所有权校验
|
||||
await deleteAiProviderRecord(
|
||||
parsed.data.id,
|
||||
user.isAdmin ? undefined : user.id
|
||||
)
|
||||
revalidatePath("/admin/ai-settings")
|
||||
revalidatePath("/settings")
|
||||
return { success: true, message: "AI provider deleted", data: null }
|
||||
@@ -210,3 +251,18 @@ export async function deleteAiProviderAction(
|
||||
return { success: false, message: "Failed to delete AI provider" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前用户是否拥有 AI_CONFIGURE 权限(管理员)
|
||||
*
|
||||
* 供 UI 层决定是否显示 public 可见性选项。
|
||||
*/
|
||||
export async function canConfigurePublicAiProvider(): Promise<ActionState<boolean>> {
|
||||
try {
|
||||
const ctx = await getAuthContext()
|
||||
return { success: true, data: ctx.permissions.includes(Permissions.AI_CONFIGURE) }
|
||||
} catch (error) {
|
||||
if (error instanceof PermissionDeniedError) return { success: false, message: error.message, data: false }
|
||||
return { success: false, message: "Failed to check permission", data: false }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user