feat: add oauth callback email binding ui
This commit is contained in:
@@ -30,6 +30,20 @@ describe('oauth adoption auth api', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('posts bind-login decisions when finalizing pending oauth bind flow', async () => {
|
||||
const { completePendingOAuthBindLogin } = await import('@/api/auth')
|
||||
|
||||
await completePendingOAuthBindLogin({
|
||||
adoptDisplayName: true,
|
||||
adoptAvatar: false
|
||||
})
|
||||
|
||||
expect(post).toHaveBeenCalledWith('/auth/oauth/pending/exchange', {
|
||||
adopt_display_name: true,
|
||||
adopt_avatar: false
|
||||
})
|
||||
})
|
||||
|
||||
it('posts linuxdo invitation completion with adoption decisions', async () => {
|
||||
const { completeLinuxDoOAuthRegistration } = await import('@/api/auth')
|
||||
|
||||
@@ -45,6 +59,21 @@ describe('oauth adoption auth api', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('posts linuxdo create-account completion with adoption decisions', async () => {
|
||||
const { createPendingLinuxDoOAuthAccount } = await import('@/api/auth')
|
||||
|
||||
await createPendingLinuxDoOAuthAccount('invite-code', {
|
||||
adoptDisplayName: false,
|
||||
adoptAvatar: true
|
||||
})
|
||||
|
||||
expect(post).toHaveBeenCalledWith('/auth/oauth/linuxdo/complete-registration', {
|
||||
invitation_code: 'invite-code',
|
||||
adopt_display_name: false,
|
||||
adopt_avatar: true
|
||||
})
|
||||
})
|
||||
|
||||
it('posts oidc invitation completion with adoption decisions', async () => {
|
||||
const { completeOIDCOAuthRegistration } = await import('@/api/auth')
|
||||
|
||||
@@ -60,6 +89,21 @@ describe('oauth adoption auth api', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('posts oidc create-account completion with adoption decisions', async () => {
|
||||
const { createPendingOIDCOAuthAccount } = await import('@/api/auth')
|
||||
|
||||
await createPendingOIDCOAuthAccount('invite-code', {
|
||||
adoptDisplayName: true,
|
||||
adoptAvatar: false
|
||||
})
|
||||
|
||||
expect(post).toHaveBeenCalledWith('/auth/oauth/oidc/complete-registration', {
|
||||
invitation_code: 'invite-code',
|
||||
adopt_display_name: true,
|
||||
adopt_avatar: false
|
||||
})
|
||||
})
|
||||
|
||||
it('posts wechat invitation completion with adoption decisions', async () => {
|
||||
const { completeWeChatOAuthRegistration } = await import('@/api/auth')
|
||||
|
||||
@@ -75,6 +119,21 @@ describe('oauth adoption auth api', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('posts wechat create-account completion with adoption decisions', async () => {
|
||||
const { createPendingWeChatOAuthAccount } = await import('@/api/auth')
|
||||
|
||||
await createPendingWeChatOAuthAccount('invite-code', {
|
||||
adoptDisplayName: false,
|
||||
adoptAvatar: false
|
||||
})
|
||||
|
||||
expect(post).toHaveBeenCalledWith('/auth/oauth/wechat/complete-registration', {
|
||||
invitation_code: 'invite-code',
|
||||
adopt_display_name: false,
|
||||
adopt_avatar: false
|
||||
})
|
||||
})
|
||||
|
||||
it('classifies oauth completion results as login or bind', async () => {
|
||||
const { getOAuthCompletionKind } = await import('@/api/auth')
|
||||
|
||||
@@ -82,6 +141,38 @@ describe('oauth adoption auth api', () => {
|
||||
expect(getOAuthCompletionKind({ redirect: '/profile' })).toBe('bind')
|
||||
})
|
||||
|
||||
it('provides bind-login utility helpers for invitation and suggested profile states', async () => {
|
||||
const {
|
||||
getPendingOAuthBindLoginKind,
|
||||
hasPendingOAuthSuggestedProfile,
|
||||
isPendingOAuthCreateAccountRequired
|
||||
} = await import('@/api/auth')
|
||||
|
||||
expect(getPendingOAuthBindLoginKind({ access_token: 'access-token' })).toBe('login')
|
||||
expect(getPendingOAuthBindLoginKind({ redirect: '/profile' })).toBe('bind')
|
||||
expect(
|
||||
isPendingOAuthCreateAccountRequired({
|
||||
error: 'invitation_required'
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
isPendingOAuthCreateAccountRequired({
|
||||
error: 'other'
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
hasPendingOAuthSuggestedProfile({
|
||||
suggested_display_name: 'OAuth Nick'
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
hasPendingOAuthSuggestedProfile({
|
||||
suggested_avatar_url: 'https://cdn.example/avatar.png'
|
||||
})
|
||||
).toBe(true)
|
||||
expect(hasPendingOAuthSuggestedProfile({})).toBe(false)
|
||||
})
|
||||
|
||||
it('prepares an oauth bind access token cookie before redirect binding', async () => {
|
||||
localStorage.setItem('auth_token', 'access-token-value')
|
||||
const setCookie = vi.fn()
|
||||
|
||||
@@ -193,7 +193,7 @@ export interface OAuthTokenResponse {
|
||||
token_type?: string
|
||||
}
|
||||
|
||||
export interface PendingOAuthExchangeResponse extends Partial<OAuthTokenResponse> {
|
||||
export interface PendingOAuthBindLoginResponse extends Partial<OAuthTokenResponse> {
|
||||
redirect?: string
|
||||
error?: string
|
||||
adoption_required?: boolean
|
||||
@@ -201,6 +201,10 @@ export interface PendingOAuthExchangeResponse extends Partial<OAuthTokenResponse
|
||||
suggested_avatar_url?: string
|
||||
}
|
||||
|
||||
export type PendingOAuthExchangeResponse = PendingOAuthBindLoginResponse
|
||||
|
||||
export interface PendingOAuthCreateAccountResponse extends OAuthTokenResponse {}
|
||||
|
||||
export type OAuthCompletionKind = 'login' | 'bind'
|
||||
|
||||
export interface OAuthAdoptionDecision {
|
||||
@@ -235,6 +239,27 @@ export function getOAuthCompletionKind(
|
||||
return isOAuthLoginCompletion(completion) ? 'login' : 'bind'
|
||||
}
|
||||
|
||||
export function getPendingOAuthBindLoginKind(
|
||||
completion: PendingOAuthBindLoginResponse
|
||||
): OAuthCompletionKind {
|
||||
return getOAuthCompletionKind(completion)
|
||||
}
|
||||
|
||||
export function isPendingOAuthCreateAccountRequired(
|
||||
completion: Pick<PendingOAuthBindLoginResponse, 'error'>
|
||||
): boolean {
|
||||
return completion.error === 'invitation_required'
|
||||
}
|
||||
|
||||
export function hasPendingOAuthSuggestedProfile(
|
||||
completion: Pick<
|
||||
PendingOAuthBindLoginResponse,
|
||||
'suggested_display_name' | 'suggested_avatar_url'
|
||||
>
|
||||
): boolean {
|
||||
return Boolean(completion.suggested_display_name || completion.suggested_avatar_url)
|
||||
}
|
||||
|
||||
export function persistOAuthTokenContext(tokens: Partial<OAuthTokenResponse>): void {
|
||||
if (tokens.refresh_token) {
|
||||
setRefreshToken(tokens.refresh_token)
|
||||
@@ -431,11 +456,7 @@ export async function completeLinuxDoOAuthRegistration(
|
||||
invitationCode: string,
|
||||
decision?: OAuthAdoptionDecision
|
||||
): Promise<OAuthTokenResponse> {
|
||||
const { data } = await apiClient.post<OAuthTokenResponse>('/auth/oauth/linuxdo/complete-registration', {
|
||||
invitation_code: invitationCode,
|
||||
...serializeOAuthAdoptionDecision(decision)
|
||||
})
|
||||
return data
|
||||
return createPendingLinuxDoOAuthAccount(invitationCode, decision)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -447,32 +468,66 @@ export async function completeOIDCOAuthRegistration(
|
||||
invitationCode: string,
|
||||
decision?: OAuthAdoptionDecision
|
||||
): Promise<OAuthTokenResponse> {
|
||||
const { data } = await apiClient.post<OAuthTokenResponse>('/auth/oauth/oidc/complete-registration', {
|
||||
invitation_code: invitationCode,
|
||||
...serializeOAuthAdoptionDecision(decision)
|
||||
})
|
||||
return data
|
||||
return createPendingOIDCOAuthAccount(invitationCode, decision)
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
return createPendingWeChatOAuthAccount(invitationCode, decision)
|
||||
}
|
||||
|
||||
async function createPendingOAuthAccount(
|
||||
provider: 'linuxdo' | 'oidc' | 'wechat',
|
||||
invitationCode: string,
|
||||
decision?: OAuthAdoptionDecision
|
||||
): Promise<PendingOAuthCreateAccountResponse> {
|
||||
const { data } = await apiClient.post<PendingOAuthCreateAccountResponse>(
|
||||
`/auth/oauth/${provider}/complete-registration`,
|
||||
{
|
||||
invitation_code: invitationCode,
|
||||
...serializeOAuthAdoptionDecision(decision)
|
||||
}
|
||||
)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function createPendingLinuxDoOAuthAccount(
|
||||
invitationCode: string,
|
||||
decision?: OAuthAdoptionDecision
|
||||
): Promise<PendingOAuthCreateAccountResponse> {
|
||||
return createPendingOAuthAccount('linuxdo', invitationCode, decision)
|
||||
}
|
||||
|
||||
export async function createPendingOIDCOAuthAccount(
|
||||
invitationCode: string,
|
||||
decision?: OAuthAdoptionDecision
|
||||
): Promise<PendingOAuthCreateAccountResponse> {
|
||||
return createPendingOAuthAccount('oidc', invitationCode, decision)
|
||||
}
|
||||
|
||||
export async function createPendingWeChatOAuthAccount(
|
||||
invitationCode: string,
|
||||
decision?: OAuthAdoptionDecision
|
||||
): Promise<PendingOAuthCreateAccountResponse> {
|
||||
return createPendingOAuthAccount('wechat', invitationCode, decision)
|
||||
}
|
||||
|
||||
export async function completePendingOAuthBindLogin(
|
||||
decision?: OAuthAdoptionDecision
|
||||
): Promise<PendingOAuthBindLoginResponse> {
|
||||
const { data } = await apiClient.post<PendingOAuthBindLoginResponse>(
|
||||
'/auth/oauth/pending/exchange',
|
||||
serializeOAuthAdoptionDecision(decision)
|
||||
)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function exchangePendingOAuthCompletion(
|
||||
decision?: OAuthAdoptionDecision
|
||||
): Promise<PendingOAuthExchangeResponse> {
|
||||
const { data } = await apiClient.post<PendingOAuthExchangeResponse>(
|
||||
'/auth/oauth/pending/exchange',
|
||||
serializeOAuthAdoptionDecision(decision)
|
||||
)
|
||||
return data
|
||||
return completePendingOAuthBindLogin(decision)
|
||||
}
|
||||
|
||||
export const authAPI = {
|
||||
@@ -498,6 +553,13 @@ export const authAPI = {
|
||||
resetPassword,
|
||||
refreshToken,
|
||||
revokeAllSessions,
|
||||
getPendingOAuthBindLoginKind,
|
||||
isPendingOAuthCreateAccountRequired,
|
||||
hasPendingOAuthSuggestedProfile,
|
||||
completePendingOAuthBindLogin,
|
||||
createPendingLinuxDoOAuthAccount,
|
||||
createPendingOIDCOAuthAccount,
|
||||
createPendingWeChatOAuthAccount,
|
||||
exchangePendingOAuthCompletion,
|
||||
completeLinuxDoOAuthRegistration,
|
||||
completeOIDCOAuthRegistration,
|
||||
|
||||
Reference in New Issue
Block a user