fix(auth): establish Manager session cookie after Heicode password login

External IdP login alone did not set Gin session; proxied API calls returned 401 and triggered session-expired toast. Call POST /api/user/login after token exchange, support Turnstile on sign-in, handle 2FA pending session, and clear Manager cookie on logout.

Made-with: Cursor
This commit is contained in:
gongzhiyong
2026-05-01 01:59:26 +08:00
parent 1f21309597
commit 0b4d4f811e
2 changed files with 80 additions and 1 deletions
+57
View File
@@ -1,5 +1,6 @@
import { api } from '@/lib/api'
import { resetHeicodeAuthenticatedSession } from '@/features/auth/heicode-authenticated-session'
import { saveUserId } from '@/features/auth/lib/storage'
import type {
LoginPayload,
LoginResponse,
@@ -44,6 +45,40 @@ export function clearHeicodeTokens() {
resetHeicodeAuthenticatedSession()
}
/** 在外部 Heicode 登录成功后,向本站点 Manager 写入会话 Cookie(否则 /api/* 会 401 →「会话已过期」) */
async function establishManagerCookieSession(payload: LoginPayload) {
const params =
payload.turnstile && payload.turnstile.length > 0
? { turnstile: payload.turnstile }
: undefined
const res = await api.post(
'/api/user/login',
{
username: payload.username,
password: payload.password,
},
{
params,
skipBusinessError: true,
skipErrorHandler: true,
} as Record<string, unknown>
)
const body = res.data as {
success?: boolean
message?: string
data?: { require_2fa?: boolean; id?: number }
}
if (!body?.success) {
throw new Error(body?.message || 'Unable to establish Manager session')
}
if (body.data?.require_2fa) {
throw new Error('TWO_FACTOR_REQUIRED')
}
if (body.data?.id != null) {
saveUserId(body.data.id)
}
}
async function callHeicodeAuth<T>(
path: string,
init: RequestInit = {},
@@ -113,6 +148,18 @@ export async function login(payload: LoginPayload) {
})
if (res?.success) {
writeTokens(res.data?.token, res.data?.refreshToken)
try {
await establishManagerCookieSession(payload)
} catch (syncErr) {
if (
syncErr instanceof Error &&
syncErr.message === 'TWO_FACTOR_REQUIRED'
) {
throw syncErr
}
clearHeicodeTokens()
throw syncErr
}
} else if (res?.detail || res?.message) {
throw new Error(res.detail || res.message || 'Login failed')
}
@@ -134,8 +181,18 @@ export async function login2fa(payload: TwoFAPayload) {
// User logout
export async function logout(): Promise<ApiResponse> {
try {
await api.get('/api/user/logout', {
skipErrorHandler: true,
skipBusinessError: true,
} as Record<string, unknown>)
} catch {
/* empty */
}
try {
await callHeicodeAuth('/api/auth/logout', { method: 'POST' })
} catch {
/* empty */
} finally {
clearHeicodeTokens()
}
@@ -20,6 +20,8 @@ import { PasswordInput } from '@/components/password-input'
import { login } from '@/features/auth/api'
import { loginFormSchema } from '@/features/auth/constants'
import { useAuthRedirect } from '@/features/auth/hooks/use-auth-redirect'
import { useTurnstile } from '@/features/auth/hooks/use-turnstile'
import { Turnstile } from '@/components/turnstile'
import type { AuthFormProps } from '@/features/auth/types'
export function UserAuthForm({
@@ -30,7 +32,14 @@ export function UserAuthForm({
const { t } = useTranslation()
const [isLoading, setIsLoading] = useState(false)
const loginFailedMessage = t('Login failed')
const { handleLoginSuccess } = useAuthRedirect()
const { handleLoginSuccess, redirectTo2FA } = useAuthRedirect()
const {
isTurnstileEnabled,
turnstileSiteKey,
turnstileToken,
setTurnstileToken,
validateTurnstile,
} = useTurnstile()
const form = useForm<z.infer<typeof loginFormSchema>>({
resolver: zodResolver(loginFormSchema),
@@ -41,11 +50,13 @@ export function UserAuthForm({
})
async function onSubmit(data: z.infer<typeof loginFormSchema>) {
if (!validateTurnstile()) return
setIsLoading(true)
try {
const res = await login({
username: data.username,
password: data.password,
turnstile: turnstileToken || undefined,
})
if (res.success) {
@@ -67,6 +78,13 @@ export function UserAuthForm({
toast.error(res.message)
}
} catch (error) {
if (
error instanceof Error &&
error.message === 'TWO_FACTOR_REQUIRED'
) {
redirectTo2FA()
return
}
if (error instanceof Error) {
toast.error(error.message)
} else {
@@ -132,6 +150,10 @@ export function UserAuthForm({
)}
/>
{isTurnstileEnabled && (
<Turnstile siteKey={turnstileSiteKey} onVerify={setTurnstileToken} />
)}
<Button
type='submit'
className='h-12 w-full justify-center gap-2 rounded-xl text-sm font-semibold shadow-[0_18px_48px_-22px_color-mix(in_oklch,var(--primary)_75%,black)]'