feat: add profile auth identity binding flow
This commit is contained in:
@@ -140,27 +140,16 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { AuthLayout } from '@/components/layout'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { apiClient } from '@/api/client'
|
||||
import { useAuthStore, useAppStore } from '@/stores'
|
||||
|
||||
interface OAuthTokenResponse {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
token_type: string
|
||||
}
|
||||
|
||||
interface PendingOAuthExchangeResponse {
|
||||
access_token?: string
|
||||
refresh_token?: string
|
||||
expires_in?: number
|
||||
token_type?: string
|
||||
redirect?: string
|
||||
error?: string
|
||||
adoption_required?: boolean
|
||||
suggested_display_name?: string
|
||||
suggested_avatar_url?: string
|
||||
}
|
||||
import {
|
||||
completeWeChatOAuthRegistration,
|
||||
exchangePendingOAuthCompletion,
|
||||
getOAuthCompletionKind,
|
||||
isOAuthLoginCompletion,
|
||||
persistOAuthTokenContext,
|
||||
type OAuthAdoptionDecision,
|
||||
type PendingOAuthExchangeResponse
|
||||
} from '@/api/auth'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -182,6 +171,7 @@ const suggestedAvatarUrl = ref('')
|
||||
const adoptDisplayName = ref(true)
|
||||
const adoptAvatar = ref(true)
|
||||
const needsAdoptionConfirmation = ref(false)
|
||||
const bindSuccessMessage = t('profile.authBindings.bindSuccess')
|
||||
|
||||
const providerName = 'WeChat'
|
||||
|
||||
@@ -200,10 +190,10 @@ function sanitizeRedirectPath(path: string | null | undefined): string {
|
||||
return path
|
||||
}
|
||||
|
||||
function currentAdoptionDecision(): Record<string, boolean> {
|
||||
function currentAdoptionDecision(): OAuthAdoptionDecision {
|
||||
return {
|
||||
adopt_display_name: adoptDisplayName.value,
|
||||
adopt_avatar: adoptAvatar.value,
|
||||
adoptDisplayName: adoptDisplayName.value,
|
||||
adoptAvatar: adoptAvatar.value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,49 +214,35 @@ function hasSuggestedProfile(completion: PendingOAuthExchangeResponse): boolean
|
||||
return Boolean(completion.suggested_display_name || completion.suggested_avatar_url)
|
||||
}
|
||||
|
||||
async function exchangePendingOAuthCompletion(): Promise<PendingOAuthExchangeResponse> {
|
||||
const { data } = await apiClient.post<PendingOAuthExchangeResponse>('/auth/oauth/pending/exchange', {})
|
||||
return data
|
||||
}
|
||||
async function finalizeCompletion(completion: PendingOAuthExchangeResponse, redirect: string) {
|
||||
if (getOAuthCompletionKind(completion) === 'bind') {
|
||||
const bindRedirect = sanitizeRedirectPath(completion.redirect || '/profile')
|
||||
appStore.showSuccess(bindSuccessMessage)
|
||||
await router.replace(bindRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
async function finalizeLogin(completion: PendingOAuthExchangeResponse, redirect: string) {
|
||||
if (!completion.access_token) {
|
||||
if (!isOAuthLoginCompletion(completion)) {
|
||||
throw new Error(t('auth.oidc.callbackMissingToken'))
|
||||
}
|
||||
|
||||
if (completion.refresh_token) {
|
||||
localStorage.setItem('refresh_token', completion.refresh_token)
|
||||
}
|
||||
if (completion.expires_in) {
|
||||
localStorage.setItem('token_expires_at', String(Date.now() + completion.expires_in * 1000))
|
||||
}
|
||||
|
||||
persistOAuthTokenContext(completion)
|
||||
await authStore.setToken(completion.access_token)
|
||||
appStore.showSuccess(t('auth.loginSuccess'))
|
||||
await router.replace(redirect)
|
||||
}
|
||||
|
||||
async function completeWeChatOAuthRegistration(invitation: string): Promise<OAuthTokenResponse> {
|
||||
const { data } = await apiClient.post<OAuthTokenResponse>('/auth/oauth/wechat/complete-registration', {
|
||||
invitation_code: invitation,
|
||||
...currentAdoptionDecision(),
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
async function handleSubmitInvitation() {
|
||||
invitationError.value = ''
|
||||
if (!invitationCode.value.trim()) return
|
||||
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
const tokenData = await completeWeChatOAuthRegistration(invitationCode.value.trim())
|
||||
if (tokenData.refresh_token) {
|
||||
localStorage.setItem('refresh_token', tokenData.refresh_token)
|
||||
}
|
||||
if (tokenData.expires_in) {
|
||||
localStorage.setItem('token_expires_at', String(Date.now() + tokenData.expires_in * 1000))
|
||||
}
|
||||
const tokenData = await completeWeChatOAuthRegistration(
|
||||
invitationCode.value.trim(),
|
||||
currentAdoptionDecision()
|
||||
)
|
||||
persistOAuthTokenContext(tokenData)
|
||||
await authStore.setToken(tokenData.access_token)
|
||||
appStore.showSuccess(t('auth.loginSuccess'))
|
||||
await router.replace(redirectTo.value)
|
||||
@@ -282,11 +258,8 @@ async function handleSubmitInvitation() {
|
||||
async function handleContinueLogin() {
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
const { data } = await apiClient.post<PendingOAuthExchangeResponse>(
|
||||
'/auth/oauth/pending/exchange',
|
||||
currentAdoptionDecision()
|
||||
)
|
||||
await finalizeLogin(data, redirectTo.value)
|
||||
const completion = await exchangePendingOAuthCompletion(currentAdoptionDecision())
|
||||
await finalizeCompletion(completion, redirectTo.value)
|
||||
} catch (e: unknown) {
|
||||
const err = e as { message?: string; response?: { data?: { detail?: string; message?: string } } }
|
||||
errorMessage.value =
|
||||
@@ -333,7 +306,7 @@ onMounted(async () => {
|
||||
return
|
||||
}
|
||||
|
||||
await finalizeLogin(completion, redirect)
|
||||
await finalizeCompletion(completion, redirect)
|
||||
} catch (e: unknown) {
|
||||
const err = e as { message?: string; response?: { data?: { detail?: string; message?: string } } }
|
||||
errorMessage.value =
|
||||
|
||||
Reference in New Issue
Block a user