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:
+57
@@ -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()
|
||||
}
|
||||
|
||||
+23
-1
@@ -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)]'
|
||||
|
||||
Reference in New Issue
Block a user