mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-09-20 10:10:14 +00:00
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:
@@ -0,0 +1,96 @@
|
||||
import { act, render } from "@testing-library/react"
|
||||
import { useEffect } from "react"
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vitest"
|
||||
|
||||
let profileStore: { id: number; role: number } | undefined
|
||||
const setProfileSpy = vi.fn((p: any) => {
|
||||
profileStore = p
|
||||
})
|
||||
|
||||
vi.mock("./useMainStore", () => ({}))
|
||||
vi.mock("@/hooks/useMainStore", () => ({
|
||||
useMainStore: (selector: any) =>
|
||||
selector({ profile: profileStore, setProfile: setProfileSpy }),
|
||||
}))
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({ t: (k: string) => k }),
|
||||
}))
|
||||
|
||||
vi.mock("sonner", () => ({ toast: () => {} }))
|
||||
|
||||
const navigate = vi.fn()
|
||||
vi.mock("react-router-dom", () => ({
|
||||
useNavigate: () => navigate,
|
||||
}))
|
||||
|
||||
// Deferred initial getProfile() so we can resolve/reject it AFTER login().
|
||||
let rejectInitial: (e: any) => void
|
||||
const initialProfilePromise = new Promise((_res, rej) => {
|
||||
rejectInitial = rej
|
||||
})
|
||||
let getProfileCall = 0
|
||||
const loginRequest = vi.fn(async () => {})
|
||||
vi.mock("@/api/user", () => ({
|
||||
getProfile: vi.fn(() => {
|
||||
getProfileCall++
|
||||
if (getProfileCall === 1) return initialProfilePromise
|
||||
return Promise.resolve({ id: 42, role: 0 })
|
||||
}),
|
||||
login: () => loginRequest(),
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
profileStore = undefined
|
||||
getProfileCall = 0
|
||||
setProfileSpy.mockClear()
|
||||
navigate.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ""
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// The AuthProvider fires getProfile() on mount. While that probe is in flight a
|
||||
// user can submit the login form (ProtectedRoute renders the login page during
|
||||
// loading). If the in-flight probe later REJECTS (e.g. it 401'd because the
|
||||
// user was not yet authenticated), its catch{} must NOT clobber the profile a
|
||||
// successful login() already set — otherwise the freshly-authenticated user is
|
||||
// bounced back to the login page.
|
||||
test("late-rejecting initial profile probe does not clobber a successful login", async () => {
|
||||
const { AuthProvider, useAuth } = await import("@/hooks/useAuth")
|
||||
|
||||
const captured: { auth?: ReturnType<typeof useAuth> } = {}
|
||||
function Capture() {
|
||||
const auth = useAuth()
|
||||
useEffect(() => {
|
||||
captured.auth = auth
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<AuthProvider>
|
||||
<Capture />
|
||||
</AuthProvider>,
|
||||
)
|
||||
})
|
||||
|
||||
// User logs in while the initial probe is still pending.
|
||||
await act(async () => {
|
||||
await captured.auth!.login("u", "p")
|
||||
})
|
||||
expect(profileStore).toEqual({ id: 42, role: 0 })
|
||||
|
||||
// Now the stale initial probe rejects (it was a pre-auth 401).
|
||||
await act(async () => {
|
||||
rejectInitial(new Error("401"))
|
||||
await initialProfilePromise.catch(() => {})
|
||||
})
|
||||
|
||||
// The logged-in profile must survive.
|
||||
expect(profileStore).toEqual({ id: 42, role: 0 })
|
||||
expect(setProfileSpy).not.toHaveBeenLastCalledWith(undefined)
|
||||
})
|
||||
Reference in New Issue
Block a user