feat(api-tokens): add PAT management UI, CSRF handling, and auth-loading fixes

Add an API tokens management route to create, list, and revoke PATs, showing
the plaintext token once on creation with scope and server-id selection.

Mirror the nz-csrf cookie into the X-CSRF-Token header on unsafe fetcher
methods (POST/PUT/PATCH/DELETE) for the server-side double-submit check, and
self-heal expired sessions via refresh-token without a recursive fetch loop.

Gate protected routes behind resolved auth state to avoid pre-auth SWR fetches,
and fix the login loading/race so stale probes cannot clobber the session.

Add i18n keys for the new screens across all locales.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
This commit is contained in:
naiba
2026-05-30 15:56:53 +00:00
co-authored by cloudcode
parent f47d4a8772
commit f07c557029
37 changed files with 2041 additions and 69 deletions
+43 -32
View File
@@ -31,7 +31,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { Navigate } from "react-router-dom"
import { toast } from "sonner"
import { z } from "zod"
@@ -52,13 +52,13 @@ const settingFormSchema = z.object({
tls: asOptionalField(z.boolean()),
enable_ip_change_notification: asOptionalField(z.boolean()),
enable_plain_ip_in_notification: asOptionalField(z.boolean()),
enable_mcp: asOptionalField(z.boolean()),
})
export default function SettingsPage() {
const { t, i18n } = useTranslation()
const { data: config, mutate } = useSetting()
const { profile } = useAuth()
const navigate = useNavigate()
const { profile, loading: authLoading } = useAuth()
const { notifierGroup } = useNotification()
const ngroupList = notifierGroup?.map((ng) => ({
@@ -68,10 +68,7 @@ export default function SettingsPage() {
const isAdmin = profile?.role === 0
if (!isAdmin) {
navigate("/dashboard/settings/online-user")
}
// 所有 hooks 必须在条件 return 之前调用,否则违反 rules-of-hooks。
const form = useForm({
resolver: zodResolver(settingFormSchema) as any,
defaultValues: config
@@ -100,6 +97,13 @@ export default function SettingsPage() {
}
}, [config?.config, form])
if (authLoading) {
return null
}
if (!isAdmin) {
return <Navigate to="/dashboard/settings/api-tokens" replace />
}
const onSubmit = async (values: any) => {
try {
await updateSettings(values)
@@ -112,12 +116,11 @@ export default function SettingsPage() {
}),
})
return
} finally {
if (values.language != i18n.language) {
i18n.changeLanguage(values.language)
}
toast(t("Success"))
}
if (values.language != i18n.language) {
i18n.changeLanguage(values.language)
}
toast(t("Success"))
}
return (
@@ -340,16 +343,10 @@ export default function SettingsPage() {
checked={field.value == "NZ::Use-Peer-IP"}
className="ml-2"
onCheckedChange={(checked) => {
if (checked) {
field.disabled = true
form.setValue(
"web_real_ip_header",
"NZ::Use-Peer-IP",
)
} else {
field.disabled = false
form.setValue("web_real_ip_header", "")
}
form.setValue(
"web_real_ip_header",
checked ? "NZ::Use-Peer-IP" : "",
)
}}
/>
<FormLabel className="font-normal ml-2">
@@ -379,16 +376,10 @@ export default function SettingsPage() {
checked={field.value == "NZ::Use-Peer-IP"}
className="ml-2"
onCheckedChange={(checked) => {
if (checked) {
field.disabled = true
form.setValue(
"agent_real_ip_header",
"NZ::Use-Peer-IP",
)
} else {
field.disabled = false
form.setValue("agent_real_ip_header", "")
}
form.setValue(
"agent_real_ip_header",
checked ? "NZ::Use-Peer-IP" : "",
)
}}
/>
<FormLabel className="font-normal ml-2">
@@ -513,6 +504,26 @@ export default function SettingsPage() {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enable_mcp"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">
{t("EnableMCP")}
</Label>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">{t("Confirm")}</Button>
</form>
</Form>