mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 16:48:45 +08:00
feat(grok): ReAuth 弹窗支持 SSO、密码与 RT 重新授权
管理端重新授权对 Grok 展示 SSO Cookie、邮箱密码与 RT 入口; 校验成功后对现有账号 applyOAuthCredentials,不走批量建号。 密码/SSO 仅用于授权 API,经 buildCredentials 写入 OAuth 凭据。
This commit is contained in:
@@ -130,12 +130,18 @@
|
||||
:show-help="isAnthropic"
|
||||
:show-proxy-warning="isAnthropic"
|
||||
:show-cookie-option="isAnthropic"
|
||||
:show-refresh-token-option="isOpenAI || isAntigravity || isGrok"
|
||||
:show-sso-option="isGrok"
|
||||
:show-email-password-option="isGrok"
|
||||
:allow-multiple="false"
|
||||
:method-label="t('admin.accounts.inputMethod')"
|
||||
:platform="isOpenAI ? 'openai' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : isGrok ? 'grok' : 'anthropic'"
|
||||
:show-project-id="isGemini && geminiOAuthType === 'code_assist'"
|
||||
@generate-url="handleGenerateUrl"
|
||||
@cookie-auth="handleCookieAuth"
|
||||
@validate-refresh-token="handleGrokValidateRefreshToken"
|
||||
@import-sso="handleGrokImportSSO"
|
||||
@authorize-password="handleGrokAuthorizePassword"
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -279,10 +285,20 @@ const currentError = computed(() => {
|
||||
return claudeOAuth.error.value
|
||||
})
|
||||
|
||||
// Computed
|
||||
// Computed — footer "complete auth" only for code-exchange flows, not SSO/password/RT.
|
||||
const isManualInputMethod = computed(() => {
|
||||
// OpenAI/Gemini/Antigravity always use manual input (no cookie auth option)
|
||||
return isOpenAILike.value || isGemini.value || isAntigravity.value || isGrok.value || oauthFlowRef.value?.inputMethod === 'manual'
|
||||
const method = oauthFlowRef.value?.inputMethod
|
||||
if (method === 'sso_cookie' || method === 'email_password' || method === 'refresh_token') {
|
||||
return false
|
||||
}
|
||||
// OpenAI/Gemini/Antigravity/Grok use manual code paste by default (no cookie auth)
|
||||
return (
|
||||
isOpenAILike.value ||
|
||||
isGemini.value ||
|
||||
isAntigravity.value ||
|
||||
isGrok.value ||
|
||||
method === 'manual'
|
||||
)
|
||||
})
|
||||
|
||||
const canExchangeCode = computed(() => {
|
||||
@@ -574,4 +590,111 @@ const handleCookieAuth = async (sessionKey: string) => {
|
||||
claudeOAuth.loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply Grok Build OAuth tokens onto the existing account (never store password/SSO). */
|
||||
const applyGrokReauthTokenInfo = async (tokenInfo: {
|
||||
access_token?: string
|
||||
refresh_token?: string
|
||||
email?: string
|
||||
[key: string]: unknown
|
||||
}) => {
|
||||
if (!props.account) return
|
||||
const credentials = grokOAuth.buildCredentials(tokenInfo as any)
|
||||
const extra = grokOAuth.buildExtraInfo(tokenInfo as any)
|
||||
const updatedAccount = await adminAPI.accounts.applyOAuthCredentials(props.account.id, {
|
||||
type: 'oauth',
|
||||
credentials,
|
||||
extra
|
||||
})
|
||||
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
|
||||
emit('reauthorized', updatedAccount)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
/** Re-auth with a single SSO cookie → Build OAuth (not batch create). */
|
||||
const handleGrokImportSSO = async (ssoInput: string) => {
|
||||
if (!props.account || !isGrok.value) return
|
||||
const ssoToken = ssoInput
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)[0]
|
||||
if (!ssoToken) return
|
||||
|
||||
grokOAuth.loading.value = true
|
||||
grokOAuth.error.value = ''
|
||||
try {
|
||||
const tokenInfo = await grokOAuth.validateSSOToken(ssoToken, props.account.proxy_id)
|
||||
if (!tokenInfo) return
|
||||
await applyGrokReauthTokenInfo(tokenInfo)
|
||||
} catch (error: any) {
|
||||
grokOAuth.error.value =
|
||||
error.response?.data?.detail ||
|
||||
error.message ||
|
||||
t('admin.accounts.oauth.grok.failedToValidateSSO', 'Failed to validate Grok SSO')
|
||||
appStore.showError(grokOAuth.error.value)
|
||||
} finally {
|
||||
grokOAuth.loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-auth with email----password (single line; password never persisted). */
|
||||
const handleGrokAuthorizePassword = async (emailPasswordInput: string) => {
|
||||
if (!props.account || !isGrok.value) return
|
||||
const line = emailPasswordInput
|
||||
.split('\n')
|
||||
.map((item) => item.trim())
|
||||
.find((item) => item.includes('----'))
|
||||
if (!line) {
|
||||
grokOAuth.error.value = t(
|
||||
'admin.accounts.oauth.grok.pleaseEnterPassword',
|
||||
'Please enter email----password'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
grokOAuth.loading.value = true
|
||||
grokOAuth.error.value = ''
|
||||
try {
|
||||
const tokenInfo = await grokOAuth.authorizePassword(line, props.account.proxy_id)
|
||||
if (!tokenInfo) return
|
||||
await applyGrokReauthTokenInfo(tokenInfo)
|
||||
} catch (error: any) {
|
||||
grokOAuth.error.value =
|
||||
error.response?.data?.detail ||
|
||||
error.message ||
|
||||
t('admin.accounts.oauth.grok.failedToAuthorizePassword', 'Password authorization failed')
|
||||
appStore.showError(grokOAuth.error.value)
|
||||
} finally {
|
||||
grokOAuth.loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-auth with a single refresh token. */
|
||||
const handleGrokValidateRefreshToken = async (refreshTokenInput: string) => {
|
||||
if (!props.account || !isGrok.value) return
|
||||
const refreshToken = refreshTokenInput
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)[0]
|
||||
if (!refreshToken) {
|
||||
grokOAuth.error.value = t('admin.accounts.oauth.grok.pleaseEnterRefreshToken')
|
||||
return
|
||||
}
|
||||
|
||||
grokOAuth.loading.value = true
|
||||
grokOAuth.error.value = ''
|
||||
try {
|
||||
const tokenInfo = await grokOAuth.validateRefreshToken(refreshToken, props.account.proxy_id)
|
||||
if (!tokenInfo) return
|
||||
await applyGrokReauthTokenInfo(tokenInfo)
|
||||
} catch (error: any) {
|
||||
grokOAuth.error.value =
|
||||
error.response?.data?.detail ||
|
||||
error.message ||
|
||||
t('admin.accounts.oauth.grok.failedToValidateRT')
|
||||
appStore.showError(grokOAuth.error.value)
|
||||
} finally {
|
||||
grokOAuth.loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(
|
||||
resolve(process.cwd(), 'src/components/admin/account/ReAuthAccountModal.vue'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
describe('ReAuthAccountModal Grok re-auth paths', () => {
|
||||
it('exposes SSO cookie, email-password, and refresh-token options for Grok', () => {
|
||||
expect(source).toContain(':show-sso-option="isGrok"')
|
||||
expect(source).toContain(':show-email-password-option="isGrok"')
|
||||
expect(source).toContain(':show-refresh-token-option="isOpenAI || isAntigravity || isGrok"')
|
||||
})
|
||||
|
||||
it('wires password and SSO handlers without batch account create', () => {
|
||||
expect(source).toContain('@authorize-password="handleGrokAuthorizePassword"')
|
||||
expect(source).toContain('@import-sso="handleGrokImportSSO"')
|
||||
expect(source).toContain('@validate-refresh-token="handleGrokValidateRefreshToken"')
|
||||
expect(source).toContain('handleGrokAuthorizePassword')
|
||||
expect(source).toContain('grokOAuth.authorizePassword')
|
||||
expect(source).toContain('grokOAuth.validateSSOToken')
|
||||
expect(source).toContain('grokOAuth.buildCredentials')
|
||||
// Re-auth updates the existing account; must not call createFromSSO batch create
|
||||
expect(source).not.toContain('createFromSSO')
|
||||
expect(source).toContain('applyOAuthCredentials')
|
||||
})
|
||||
|
||||
it('hides footer code-exchange button for SSO/password/RT input methods', () => {
|
||||
expect(source).toContain("method === 'sso_cookie'")
|
||||
expect(source).toContain("method === 'email_password'")
|
||||
expect(source).toContain("method === 'refresh_token'")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user