feat(settings): add security center, 2FA/TOTP, avatar upload, system settings

- Add TOTP implementation and two-factor data-access for 2FA enrollment

- Add security center card with password policy and session management

- Add avatar upload action and component

- Add system settings actions and data-access (actions-system-settings, data-access-system-settings)

- Add notification preferences and service actions

- Add security-utils and student-overview-data with tests

- Update existing settings views, data-access, and types for new features
This commit is contained in:
SpecialX
2026-06-23 17:37:06 +08:00
parent 242a770cc9
commit 1fcef5c3aa
22 changed files with 3091 additions and 52 deletions

View File

@@ -12,6 +12,7 @@ import { ProfileSettingsForm } from "@/modules/settings/components/profile-setti
import { PasswordChangeForm } from "@/modules/settings/components/password-change-form"
import { NotificationPreferencesForm } from "@/modules/settings/components/notification-preferences-form"
import { AiProviderSettingsCard } from "@/modules/settings/components/ai-provider-settings-card"
import { SecurityCenterCard } from "@/modules/settings/components/security-center-card"
import { SettingsSectionErrorBoundary } from "@/modules/settings/components/settings-section-error-boundary"
import { Button } from "@/shared/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/ui/card"
@@ -44,6 +45,8 @@ interface SettingsViewProps {
notificationPreferences: NotificationPreferences
/** General 标签页中 ProfileSettingsForm 下方的内容(角色专属快捷链接等) */
generalExtra?: ReactNode
/** 当前请求的 User-Agent用于安全中心标记当前会话 */
currentUserAgent?: string
}
const VALID_TABS = ["general", "notifications", "appearance", "security", "ai"] as const
@@ -87,6 +90,7 @@ function SettingsViewInner({
user,
notificationPreferences,
generalExtra,
currentUserAgent,
}: SettingsViewProps) {
const t = useTranslations("settings")
const router = useRouter()
@@ -94,7 +98,15 @@ function SettingsViewInner({
const { hasPermission } = usePermission()
const tabParam = searchParams.get("tab")
const activeTab: TabValue = isTabValue(tabParam) ? tabParam : "general"
const canConfigureAi = hasPermission(Permissions.AI_CONFIGURE)
// 解析 tab 参数,对无权限的 tab如非管理员的 ai回退到 general
function resolveTab(value: string | null): TabValue {
if (!isTabValue(value)) return "general"
if (value === "ai" && !canConfigureAi) return "general"
return value
}
const activeTab: TabValue = resolveTab(tabParam)
const handleTabChange = (value: string) => {
const params = new URLSearchParams(searchParams.toString())
@@ -107,8 +119,6 @@ function SettingsViewInner({
router.push(query ? `?${query}` : "?", { scroll: false })
}
const canConfigureAi = hasPermission(Permissions.AI_CONFIGURE)
return (
<div className="flex h-full flex-col gap-8 p-8">
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-center">
@@ -178,6 +188,7 @@ function SettingsViewInner({
<SettingsSectionErrorBoundary>
<Suspense fallback={<SettingsSectionSkeleton />}>
<PasswordChangeForm />
<SecurityCenterCard currentDeviceLabel={currentUserAgent} />
<Card>
<CardHeader>
<CardTitle>{t("security.session.title")}</CardTitle>