feat: add profile auth identity binding flow

This commit is contained in:
IanShaw027
2026-04-20 18:28:44 +08:00
parent 13d9780df4
commit c6d8592484
31 changed files with 3419 additions and 239 deletions

View File

@@ -186,11 +186,14 @@ export interface RefreshTokenResponse {
token_type: string
}
export interface PendingOAuthExchangeResponse {
access_token?: string
export interface OAuthTokenResponse {
access_token: string
refresh_token?: string
expires_in?: number
token_type?: string
}
export interface PendingOAuthExchangeResponse extends Partial<OAuthTokenResponse> {
redirect?: string
error?: string
adoption_required?: boolean
@@ -198,6 +201,8 @@ export interface PendingOAuthExchangeResponse {
suggested_avatar_url?: string
}
export type OAuthCompletionKind = 'login' | 'bind'
export interface OAuthAdoptionDecision {
adoptDisplayName?: boolean
adoptAvatar?: boolean
@@ -218,6 +223,56 @@ function serializeOAuthAdoptionDecision(
return payload
}
export function isOAuthLoginCompletion(
completion: Partial<OAuthTokenResponse>
): completion is OAuthTokenResponse {
return typeof completion.access_token === 'string' && completion.access_token.trim().length > 0
}
export function getOAuthCompletionKind(
completion: Partial<OAuthTokenResponse>
): OAuthCompletionKind {
return isOAuthLoginCompletion(completion) ? 'login' : 'bind'
}
export function persistOAuthTokenContext(tokens: Partial<OAuthTokenResponse>): void {
if (tokens.refresh_token) {
setRefreshToken(tokens.refresh_token)
}
if (tokens.expires_in) {
setTokenExpiresAt(tokens.expires_in)
}
}
export function prepareOAuthBindAccessTokenCookie(): void {
if (typeof document === 'undefined' || typeof window === 'undefined') {
return
}
const token = getAuthToken()
if (!token) {
return
}
const secure = window.location.protocol === 'https:' ? '; Secure' : ''
const path = resolveOAuthBindCookiePath()
document.cookie =
`oauth_bind_access_token=${encodeURIComponent(token)}; Path=${path}/auth/oauth; Max-Age=600; SameSite=Lax${secure}`
}
function resolveOAuthBindCookiePath(): string {
const apiBase = ((import.meta.env.VITE_API_BASE_URL as string | undefined) || '/api/v1').replace(/\/$/, '')
try {
return new URL(apiBase, window.location.origin).pathname.replace(/\/$/, '') || '/api/v1'
} catch {
if (apiBase.startsWith('/')) {
return apiBase
}
return '/api/v1'
}
}
/**
* Refresh the access token using the refresh token
* @returns New token pair
@@ -375,13 +430,8 @@ export async function resetPassword(request: ResetPasswordRequest): Promise<Rese
export async function completeLinuxDoOAuthRegistration(
invitationCode: string,
decision?: OAuthAdoptionDecision
): Promise<{ access_token: string; refresh_token: string; expires_in: number; token_type: string }> {
const { data } = await apiClient.post<{
access_token: string
refresh_token: string
expires_in: number
token_type: string
}>('/auth/oauth/linuxdo/complete-registration', {
): Promise<OAuthTokenResponse> {
const { data } = await apiClient.post<OAuthTokenResponse>('/auth/oauth/linuxdo/complete-registration', {
invitation_code: invitationCode,
...serializeOAuthAdoptionDecision(decision)
})
@@ -396,13 +446,19 @@ export async function completeLinuxDoOAuthRegistration(
export async function completeOIDCOAuthRegistration(
invitationCode: string,
decision?: OAuthAdoptionDecision
): Promise<{ access_token: string; refresh_token: string; expires_in: number; token_type: string }> {
const { data } = await apiClient.post<{
access_token: string
refresh_token: string
expires_in: number
token_type: string
}>('/auth/oauth/oidc/complete-registration', {
): Promise<OAuthTokenResponse> {
const { data } = await apiClient.post<OAuthTokenResponse>('/auth/oauth/oidc/complete-registration', {
invitation_code: invitationCode,
...serializeOAuthAdoptionDecision(decision)
})
return data
}
export async function completeWeChatOAuthRegistration(
invitationCode: string,
decision?: OAuthAdoptionDecision
): Promise<OAuthTokenResponse> {
const { data } = await apiClient.post<OAuthTokenResponse>('/auth/oauth/wechat/complete-registration', {
invitation_code: invitationCode,
...serializeOAuthAdoptionDecision(decision)
})
@@ -444,7 +500,8 @@ export const authAPI = {
revokeAllSessions,
exchangePendingOAuthCompletion,
completeLinuxDoOAuthRegistration,
completeOIDCOAuthRegistration
completeOIDCOAuthRegistration,
completeWeChatOAuthRegistration
}
export default authAPI