mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-30 00:21:34 +00:00
fix(grok): tighten OAuth SSO flow and hide password login
Require oauth state/redirect consistency, fail closed on missing proxy, and remove password login from create/reauth UI (admin-only password path stays off by default).
This commit is contained in:
@@ -3212,7 +3212,7 @@
|
||||
:show-agent-identity-option="form.platform === 'openai'"
|
||||
:show-codex-pat-option="form.platform === 'openai'"
|
||||
:show-sso-option="form.platform === 'grok'"
|
||||
:show-email-password-option="form.platform === 'grok'"
|
||||
:show-email-password-option="false"
|
||||
:show-manual-option="true"
|
||||
:initial-input-method="'manual'"
|
||||
:platform="form.platform"
|
||||
@@ -3316,8 +3316,8 @@
|
||||
<BaseDialog
|
||||
:show="showGeminiHelpDialog"
|
||||
:title="t('admin.accounts.gemini.helpDialog.title')"
|
||||
width="wide"
|
||||
@close="showGeminiHelpDialog = false"
|
||||
max-width="max-w-3xl"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<!-- Setup Guide Section -->
|
||||
|
||||
@@ -24,18 +24,12 @@ describe('CreateAccountModal Grok account types', () => {
|
||||
})
|
||||
|
||||
it('validates and applies upstream config on Grok OAuth create paths', () => {
|
||||
// 授权码兑换 / RT 批量 / SSO 批量 / 密码登录(4 条路径)
|
||||
expect(source.match(/validateGrokOAuthUpstreamConfig\(\)/g)?.length).toBeGreaterThanOrEqual(4)
|
||||
expect(source.match(/applyGrokOAuthUpstreamConfig\(credentials\)/g)?.length).toBeGreaterThanOrEqual(4)
|
||||
// 授权码兑换 / RT 批量 / SSO 批量(密码授权已隐藏)
|
||||
expect(source.match(/validateGrokOAuthUpstreamConfig\(\)/g)?.length).toBeGreaterThanOrEqual(3)
|
||||
expect(source.match(/applyGrokOAuthUpstreamConfig\(credentials\)/g)?.length).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
it('wires Grok password authorize path without storing password/SSO fields', () => {
|
||||
expect(source).toContain('show-email-password-option')
|
||||
expect(source).toContain('@authorize-password="handleGrokAuthorizePassword"')
|
||||
expect(source).toContain('handleGrokAuthorizePassword')
|
||||
expect(source).toContain('grokOAuth.authorizePassword')
|
||||
expect(source).toContain('grokOAuth.buildCredentials')
|
||||
// Password only for authorize call; credentials come from buildCredentials
|
||||
expect(source).toContain('email----password')
|
||||
it('hides Grok password authorize option in the create flow', () => {
|
||||
expect(source).toContain(':show-email-password-option="false"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -132,18 +132,16 @@
|
||||
:show-cookie-option="isAnthropic"
|
||||
:show-refresh-token-option="isOpenAI || isAntigravity || isGrok"
|
||||
:show-sso-option="isGrok"
|
||||
:show-email-password-option="isGrok"
|
||||
:show-email-password-option="false"
|
||||
: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'"
|
||||
:initial-input-method="grokInitialInputMethod"
|
||||
:initial-email-password="grokPrefillEmailPassword"
|
||||
@generate-url="handleGenerateUrl"
|
||||
@cookie-auth="handleCookieAuth"
|
||||
@validate-refresh-token="handleGrokValidateRefreshToken"
|
||||
@import-sso="handleGrokImportSSO"
|
||||
@authorize-password="handleGrokAuthorizePassword"
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -257,38 +255,19 @@ const isAnthropic = computed(() => props.account?.platform === 'anthropic')
|
||||
const isAntigravity = computed(() => props.account?.platform === 'antigravity')
|
||||
const isGrok = computed(() => props.account?.platform === 'grok')
|
||||
|
||||
/** Stored Grok email for reauth prefill (password is never stored). */
|
||||
const grokAccountEmail = computed(() => {
|
||||
if (!isGrok.value || !props.account) return ''
|
||||
const creds = (props.account.credentials || {}) as Record<string, unknown>
|
||||
const email = typeof creds.email === 'string' ? creds.email.trim() : ''
|
||||
return email
|
||||
})
|
||||
|
||||
/**
|
||||
* Prefill "email----" so the operator only types the password.
|
||||
* Empty when no email is known (full email----password required).
|
||||
*/
|
||||
const grokPrefillEmailPassword = computed(() => {
|
||||
const email = grokAccountEmail.value
|
||||
return email ? `${email}----` : ''
|
||||
})
|
||||
|
||||
/**
|
||||
* Grok reauth default tab:
|
||||
* - password first when we know the email (common reauth path)
|
||||
* - refresh_token when email unknown but RT may still work
|
||||
* - email_password otherwise
|
||||
* Grok reauth default tab (password auth is hidden):
|
||||
* - refresh_token when RT may still work
|
||||
* - SSO cookie otherwise
|
||||
*/
|
||||
const grokInitialInputMethod = computed<AuthInputMethod>(() => {
|
||||
if (!isGrok.value) return 'manual'
|
||||
if (grokAccountEmail.value) return 'email_password'
|
||||
const creds = (props.account?.credentials || {}) as Record<string, unknown>
|
||||
const hasRT =
|
||||
(typeof creds.refresh_token === 'string' && creds.refresh_token.trim() !== '') ||
|
||||
(typeof creds.has_refresh_token === 'boolean' && creds.has_refresh_token)
|
||||
if (hasRT) return 'refresh_token'
|
||||
return 'email_password'
|
||||
return 'sso_cookie'
|
||||
})
|
||||
|
||||
// Computed - current OAuth state based on platform
|
||||
@@ -673,38 +652,6 @@ const handleGrokImportSSO = async (ssoInput: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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')
|
||||
// Email is normalized in the API helper, but password whitespace is valid.
|
||||
.find((item) => item.trim() && 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
|
||||
|
||||
@@ -8,18 +8,16 @@ const source = readFileSync(
|
||||
)
|
||||
|
||||
describe('ReAuthAccountModal Grok re-auth paths', () => {
|
||||
it('exposes SSO cookie, email-password, and refresh-token options for Grok', () => {
|
||||
it('exposes SSO cookie and refresh-token options; password auth stays hidden', () => {
|
||||
expect(source).toContain(':show-sso-option="isGrok"')
|
||||
expect(source).toContain(':show-email-password-option="isGrok"')
|
||||
expect(source).toContain(':show-email-password-option="false"')
|
||||
expect(source).toContain(':show-refresh-token-option="isOpenAI || isAntigravity || isGrok"')
|
||||
expect(source).not.toContain('@authorize-password=')
|
||||
})
|
||||
|
||||
it('wires password and SSO handlers without batch account create', () => {
|
||||
expect(source).toContain('@authorize-password="handleGrokAuthorizePassword"')
|
||||
it('wires SSO and RT reauth without batch account create', () => {
|
||||
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
|
||||
@@ -27,18 +25,17 @@ describe('ReAuthAccountModal Grok re-auth paths', () => {
|
||||
expect(source).toContain('applyOAuthCredentials')
|
||||
})
|
||||
|
||||
it('hides footer code-exchange button for SSO/password/RT input methods', () => {
|
||||
it('hides footer code-exchange button for SSO/RT input methods', () => {
|
||||
expect(source).toContain("method === 'sso_cookie'")
|
||||
expect(source).toContain("method === 'email_password'")
|
||||
expect(source).toContain("method === 'refresh_token'")
|
||||
})
|
||||
|
||||
it('prefills email---- and defaults to password method when email is known', () => {
|
||||
expect(source).toContain('grokPrefillEmailPassword')
|
||||
it('defaults reauth to refresh_token or sso_cookie (not password)', () => {
|
||||
expect(source).toContain('grokInitialInputMethod')
|
||||
expect(source).toContain(':initial-email-password="grokPrefillEmailPassword"')
|
||||
expect(source).toContain(':initial-input-method="grokInitialInputMethod"')
|
||||
expect(source).toContain('email----')
|
||||
expect(source).toContain("return 'email_password'")
|
||||
expect(source).toContain("return 'sso_cookie'")
|
||||
expect(source).toContain("return 'refresh_token'")
|
||||
expect(source).not.toContain("return 'email_password'")
|
||||
expect(source).not.toContain('grokPrefillEmailPassword')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user