mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-03-30 04:05:29 +00:00
Merge pull request #872 from StarryKira/fix/oauth-linuxdo-invitation-required
fix: Linux.do OAuth 注册支持邀请码两步流程 (fix #836)
This commit is contained in:
@@ -335,6 +335,28 @@ export async function resetPassword(request: ResetPasswordRequest): Promise<Rese
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete LinuxDo OAuth registration by supplying an invitation code
|
||||
* @param pendingOAuthToken - Short-lived JWT from the OAuth callback
|
||||
* @param invitationCode - Invitation code entered by the user
|
||||
* @returns Token pair on success
|
||||
*/
|
||||
export async function completeLinuxDoOAuthRegistration(
|
||||
pendingOAuthToken: string,
|
||||
invitationCode: string
|
||||
): 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', {
|
||||
pending_oauth_token: pendingOAuthToken,
|
||||
invitation_code: invitationCode
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
export const authAPI = {
|
||||
login,
|
||||
login2FA,
|
||||
@@ -357,7 +379,8 @@ export const authAPI = {
|
||||
forgotPassword,
|
||||
resetPassword,
|
||||
refreshToken,
|
||||
revokeAllSessions
|
||||
revokeAllSessions,
|
||||
completeLinuxDoOAuthRegistration
|
||||
}
|
||||
|
||||
export default authAPI
|
||||
|
||||
@@ -434,7 +434,12 @@ export default {
|
||||
callbackProcessing: 'Completing login, please wait...',
|
||||
callbackHint: 'If you are not redirected automatically, go back to the login page and try again.',
|
||||
callbackMissingToken: 'Missing login token, please try again.',
|
||||
backToLogin: 'Back to Login'
|
||||
backToLogin: 'Back to Login',
|
||||
invitationRequired: 'This Linux.do account is not yet registered. The site requires an invitation code — please enter one to complete registration.',
|
||||
invalidPendingToken: 'The registration token has expired. Please sign in with Linux.do again.',
|
||||
completeRegistration: 'Complete Registration',
|
||||
completing: 'Completing registration…',
|
||||
completeRegistrationFailed: 'Registration failed. Please check your invitation code and try again.'
|
||||
},
|
||||
oauth: {
|
||||
code: 'Code',
|
||||
|
||||
@@ -433,7 +433,12 @@ export default {
|
||||
callbackProcessing: '正在验证登录信息,请稍候...',
|
||||
callbackHint: '如果页面未自动跳转,请返回登录页重试。',
|
||||
callbackMissingToken: '登录信息缺失,请返回重试。',
|
||||
backToLogin: '返回登录'
|
||||
backToLogin: '返回登录',
|
||||
invitationRequired: '该 Linux.do 账号尚未注册,站点已开启邀请码注册,请输入邀请码以完成注册。',
|
||||
invalidPendingToken: '注册凭证已失效,请重新使用 Linux.do 登录。',
|
||||
completeRegistration: '完成注册',
|
||||
completing: '正在完成注册...',
|
||||
completeRegistrationFailed: '注册失败,请检查邀请码后重试。'
|
||||
},
|
||||
oauth: {
|
||||
code: '授权码',
|
||||
|
||||
@@ -10,6 +10,36 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="needsInvitation" class="space-y-4">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ t('auth.linuxdo.invitationRequired') }}
|
||||
</p>
|
||||
<div>
|
||||
<input
|
||||
v-model="invitationCode"
|
||||
type="text"
|
||||
class="input w-full"
|
||||
:placeholder="t('auth.invitationCodePlaceholder')"
|
||||
:disabled="isSubmitting"
|
||||
@keyup.enter="handleSubmitInvitation"
|
||||
/>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<p v-if="invitationError" class="text-sm text-red-600 dark:text-red-400">
|
||||
{{ invitationError }}
|
||||
</p>
|
||||
</transition>
|
||||
<button
|
||||
class="btn btn-primary w-full"
|
||||
:disabled="isSubmitting || !invitationCode.trim()"
|
||||
@click="handleSubmitInvitation"
|
||||
>
|
||||
{{ isSubmitting ? t('auth.linuxdo.completing') : t('auth.linuxdo.completeRegistration') }}
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
@@ -41,6 +71,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { AuthLayout } from '@/components/layout'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { useAuthStore, useAppStore } from '@/stores'
|
||||
import { completeLinuxDoOAuthRegistration } from '@/api/auth'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -52,6 +83,14 @@ const appStore = useAppStore()
|
||||
const isProcessing = ref(true)
|
||||
const errorMessage = ref('')
|
||||
|
||||
// Invitation code flow state
|
||||
const needsInvitation = ref(false)
|
||||
const pendingOAuthToken = ref('')
|
||||
const invitationCode = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
const invitationError = ref('')
|
||||
const redirectTo = ref('/dashboard')
|
||||
|
||||
function parseFragmentParams(): URLSearchParams {
|
||||
const raw = typeof window !== 'undefined' ? window.location.hash : ''
|
||||
const hash = raw.startsWith('#') ? raw.slice(1) : raw
|
||||
@@ -67,6 +106,34 @@ function sanitizeRedirectPath(path: string | null | undefined): string {
|
||||
return path
|
||||
}
|
||||
|
||||
async function handleSubmitInvitation() {
|
||||
invitationError.value = ''
|
||||
if (!invitationCode.value.trim()) return
|
||||
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
const tokenData = await completeLinuxDoOAuthRegistration(
|
||||
pendingOAuthToken.value,
|
||||
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))
|
||||
}
|
||||
await authStore.setToken(tokenData.access_token)
|
||||
appStore.showSuccess(t('auth.loginSuccess'))
|
||||
await router.replace(redirectTo.value)
|
||||
} catch (e: unknown) {
|
||||
const err = e as { message?: string; response?: { data?: { message?: string } } }
|
||||
invitationError.value =
|
||||
err.response?.data?.message || err.message || t('auth.linuxdo.completeRegistrationFailed')
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const params = parseFragmentParams()
|
||||
|
||||
@@ -80,6 +147,19 @@ onMounted(async () => {
|
||||
const errorDesc = params.get('error_description') || params.get('error_message') || ''
|
||||
|
||||
if (error) {
|
||||
if (error === 'invitation_required') {
|
||||
pendingOAuthToken.value = params.get('pending_oauth_token') || ''
|
||||
redirectTo.value = sanitizeRedirectPath(params.get('redirect'))
|
||||
if (!pendingOAuthToken.value) {
|
||||
errorMessage.value = t('auth.linuxdo.invalidPendingToken')
|
||||
appStore.showError(errorMessage.value)
|
||||
isProcessing.value = false
|
||||
return
|
||||
}
|
||||
needsInvitation.value = true
|
||||
isProcessing.value = false
|
||||
return
|
||||
}
|
||||
errorMessage.value = errorDesc || error
|
||||
appStore.showError(errorMessage.value)
|
||||
isProcessing.value = false
|
||||
|
||||
Reference in New Issue
Block a user