feat(grok): ReAuth 弹窗支持 SSO、密码与 RT 重新授权

管理端重新授权对 Grok 展示 SSO Cookie、邮箱密码与 RT 入口;
校验成功后对现有账号 applyOAuthCredentials,不走批量建号。
密码/SSO 仅用于授权 API,经 buildCredentials 写入 OAuth 凭据。
This commit is contained in:
IanShaw027
2026-08-07 13:23:03 +08:00
parent 48fc076ce1
commit 91f5b00679
2 changed files with 161 additions and 3 deletions
@@ -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'")
})
})