Seventh commit in the session-expiration-policy series. Wires the
backend taxonomy from commit 2 through to the frontend so users see
the right page (calm banner vs plain logout) when the refresh path
fails for different reasons.
- types/auth.ts: Token gains idle_expires_at + absolute_expires_at
(Optional ISO 8601 strings). The next commit adds the
useAuthSessionExpiry hook that reads these.
- api/auth.ts: OAuthCallbackResponse mirrors the same two fields.
- api/client.ts: refresh-failure handler now branches on the response
detail. session_expired_idle and session_expired_absolute both
redirect to /login?reason=session_expired (commit 8 adds the
banner that reads the query param); any other detail (most
commonly invalid_refresh_token) goes to plain /login. The bare
redirect is guarded against re-firing when the user is already on
/login. The refresh-success path now forwards the two new fields
into setTokens so the store stays current as the session ages.
- pages/OAuthCallbackPage.tsx: setTokens({...}) spreads
idle_expires_at + absolute_expires_at from the OAuth response.
No new tests — authStore.test still 2/2, tsc clean. The
useAuthSessionExpiry hook and the SessionExpiryToast that consume
the new fields land in commit 8 alongside the AccountSecurity page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
116 lines
3.3 KiB
TypeScript
116 lines
3.3 KiB
TypeScript
import apiClient from './client'
|
|
import type { Token, User, UserCreate, UserLogin, UserUpdate } from '@/types'
|
|
|
|
export interface OAuthCallbackResponse {
|
|
access_token: string
|
|
refresh_token: string
|
|
token_type: string
|
|
is_new_user: boolean
|
|
idle_expires_at?: string | null
|
|
absolute_expires_at?: string | null
|
|
}
|
|
|
|
export const authApi = {
|
|
async register(data: UserCreate): Promise<User> {
|
|
const response = await apiClient.post<User>('/auth/register', data)
|
|
return response.data
|
|
},
|
|
|
|
async login(data: UserLogin): Promise<Token> {
|
|
const response = await apiClient.post<Token>('/auth/login/json', data)
|
|
return response.data
|
|
},
|
|
|
|
async refresh(): Promise<Token> {
|
|
const refreshToken = localStorage.getItem('refresh_token')
|
|
const response = await apiClient.post<Token>('/auth/refresh', null, {
|
|
headers: {
|
|
Authorization: `Bearer ${refreshToken}`,
|
|
},
|
|
})
|
|
return response.data
|
|
},
|
|
|
|
async me(): Promise<User> {
|
|
const response = await apiClient.get<User>('/auth/me')
|
|
return response.data
|
|
},
|
|
|
|
async logout(): Promise<void> {
|
|
await apiClient.post('/auth/logout')
|
|
},
|
|
|
|
async changePassword(currentPassword: string, newPassword: string): Promise<void> {
|
|
await apiClient.post('/auth/password/change', {
|
|
current_password: currentPassword,
|
|
new_password: newPassword,
|
|
})
|
|
},
|
|
|
|
async forgotPassword(email: string): Promise<void> {
|
|
await apiClient.post('/auth/password/forgot', { email })
|
|
},
|
|
|
|
async verifyResetToken(token: string): Promise<{ valid: boolean; email: string | null }> {
|
|
const response = await apiClient.post<{ valid: boolean; email: string | null }>('/auth/password/verify-reset-token', { token })
|
|
return response.data
|
|
},
|
|
|
|
async resetPassword(token: string, newPassword: string): Promise<void> {
|
|
await apiClient.post('/auth/password/reset', {
|
|
token,
|
|
new_password: newPassword,
|
|
})
|
|
},
|
|
|
|
async updateProfile(data: UserUpdate): Promise<User> {
|
|
const response = await apiClient.patch<User>('/auth/me', data)
|
|
return response.data
|
|
},
|
|
|
|
async getVerificationStatus(): Promise<{ enabled: boolean }> {
|
|
const response = await apiClient.get<{ enabled: boolean }>('/auth/email/verification-status')
|
|
return response.data
|
|
},
|
|
|
|
async sendVerificationEmail(): Promise<void> {
|
|
await apiClient.post('/auth/email/send-verification')
|
|
},
|
|
|
|
async verifyEmail(token: string): Promise<void> {
|
|
await apiClient.post('/auth/email/verify', { token })
|
|
},
|
|
|
|
async googleCallback(
|
|
code: string,
|
|
options?: { accountInviteCode?: string; invitedEmail?: string },
|
|
): Promise<OAuthCallbackResponse> {
|
|
const response = await apiClient.post<OAuthCallbackResponse>(
|
|
'/auth/google/callback',
|
|
{
|
|
code,
|
|
account_invite_code: options?.accountInviteCode,
|
|
invited_email: options?.invitedEmail,
|
|
},
|
|
)
|
|
return response.data
|
|
},
|
|
|
|
async microsoftCallback(
|
|
code: string,
|
|
options?: { accountInviteCode?: string; invitedEmail?: string },
|
|
): Promise<OAuthCallbackResponse> {
|
|
const response = await apiClient.post<OAuthCallbackResponse>(
|
|
'/auth/microsoft/callback',
|
|
{
|
|
code,
|
|
account_invite_code: options?.accountInviteCode,
|
|
invited_email: options?.invitedEmail,
|
|
},
|
|
)
|
|
return response.data
|
|
},
|
|
}
|
|
|
|
export default authApi
|