mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-01 00:27:30 +00:00
feat(grok): 创建账号流程接入邮箱密码登录
在 OAuth 授权流中增加 email----password 入口,CreateAccount 批量调用 authorizePassword 后经 buildCredentials 建号;密码与 raw SSO 不落库。 SSO Cookie 批量导入路径保持不变。
This commit is contained in:
@@ -3212,6 +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-manual-option="true"
|
||||
:initial-input-method="'manual'"
|
||||
:platform="form.platform"
|
||||
@@ -3224,6 +3225,7 @@
|
||||
@import-codex-session="handleOpenAIImportCodexSession"
|
||||
@import-codex-pat="handleOpenAIImportCodexPAT"
|
||||
@import-sso="handleGrokImportSSO"
|
||||
@authorize-password="handleGrokAuthorizePassword"
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -5476,6 +5478,115 @@ const handleGrokImportSSO = async (ssoInput: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grok password login: each line is email----password.
|
||||
* Password is only used for the authorize API call; buildCredentials never stores it.
|
||||
*/
|
||||
const handleGrokAuthorizePassword = async (emailPasswordInput: string) => {
|
||||
if (!emailPasswordInput.trim()) return
|
||||
if (!validateGrokOAuthUpstreamConfig()) return
|
||||
|
||||
const lines = emailPasswordInput
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && line.includes('----'))
|
||||
|
||||
if (lines.length === 0) {
|
||||
grokOAuth.error.value = t(
|
||||
'admin.accounts.oauth.grok.pleaseEnterPassword',
|
||||
'Please enter email----password (one per line)'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
grokOAuth.loading.value = true
|
||||
grokOAuth.error.value = ''
|
||||
|
||||
let successCount = 0
|
||||
let failedCount = 0
|
||||
const errors: string[] = []
|
||||
|
||||
try {
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
try {
|
||||
const tokenInfo = await grokOAuth.authorizePassword(lines[i], form.proxy_id)
|
||||
if (!tokenInfo) {
|
||||
failedCount++
|
||||
errors.push(`#${i + 1}: ${grokOAuth.error.value || 'Authorization failed'}`)
|
||||
grokOAuth.error.value = ''
|
||||
continue
|
||||
}
|
||||
|
||||
const credentials = grokOAuth.buildCredentials(tokenInfo)
|
||||
applyGrokOAuthUpstreamConfig(credentials)
|
||||
const extra = grokOAuth.buildExtraInfo(tokenInfo)
|
||||
const accountName =
|
||||
lines.length > 1
|
||||
? `${form.name || tokenInfo.email || 'Grok OAuth Account'} #${i + 1}`
|
||||
: form.name || tokenInfo.email || 'Grok OAuth Account'
|
||||
|
||||
const modelMapping = buildModelMappingObject(
|
||||
modelRestrictionMode.value,
|
||||
allowedModels.value,
|
||||
modelMappings.value
|
||||
)
|
||||
if (modelMapping) {
|
||||
credentials.model_mapping = modelMapping
|
||||
}
|
||||
if (!applyTempUnschedConfig(credentials)) {
|
||||
return
|
||||
}
|
||||
|
||||
await adminAPI.accounts.create({
|
||||
name: accountName,
|
||||
notes: form.notes,
|
||||
platform: 'grok',
|
||||
type: 'oauth',
|
||||
credentials,
|
||||
extra,
|
||||
proxy_id: form.proxy_id,
|
||||
concurrency: form.concurrency,
|
||||
load_factor: form.load_factor ?? undefined,
|
||||
priority: form.priority,
|
||||
rate_multiplier: form.rate_multiplier,
|
||||
group_ids: form.group_ids,
|
||||
expires_at: form.expires_at,
|
||||
auto_pause_on_expired: autoPauseOnExpired.value
|
||||
})
|
||||
successCount++
|
||||
} catch (error: any) {
|
||||
failedCount++
|
||||
const errMsg = error.response?.data?.detail || error.message || 'Unknown error'
|
||||
errors.push(`#${i + 1}: ${errMsg}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0 && failedCount === 0) {
|
||||
appStore.showSuccess(
|
||||
lines.length > 1
|
||||
? t('admin.accounts.oauth.batchSuccess', { count: successCount })
|
||||
: t('admin.accounts.accountCreated')
|
||||
)
|
||||
emit('created')
|
||||
handleClose()
|
||||
} else if (successCount > 0) {
|
||||
appStore.showWarning(
|
||||
t('admin.accounts.oauth.batchPartialSuccess', {
|
||||
success: successCount,
|
||||
failed: failedCount
|
||||
})
|
||||
)
|
||||
grokOAuth.error.value = errors.join('\n')
|
||||
emit('created')
|
||||
} else {
|
||||
grokOAuth.error.value = errors.join('\n')
|
||||
appStore.showError(t('admin.accounts.oauth.batchFailed'))
|
||||
}
|
||||
} finally {
|
||||
grokOAuth.loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI OAuth 授权码兑换
|
||||
const handleOpenAIExchange = async (authCode: string) => {
|
||||
const oauthClient = openaiOAuth
|
||||
|
||||
@@ -59,6 +59,17 @@
|
||||
t(getOAuthKey('ssoCookieAuth'))
|
||||
}}</span>
|
||||
</label>
|
||||
<label v-if="showEmailPasswordOption" class="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
v-model="inputMethod"
|
||||
type="radio"
|
||||
value="email_password"
|
||||
class="text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span class="text-sm text-blue-900 dark:text-blue-200">{{
|
||||
t(getOAuthKey('emailPasswordAuth'))
|
||||
}}</span>
|
||||
</label>
|
||||
<label v-if="showMobileRefreshTokenOption" class="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
v-model="inputMethod"
|
||||
@@ -287,6 +298,79 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grok email + password → ephemeral SSO → Build OAuth (password never stored) -->
|
||||
<div v-if="inputMethod === 'email_password'" class="space-y-4">
|
||||
<div
|
||||
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
|
||||
>
|
||||
<p class="mb-3 text-sm text-blue-700 dark:text-blue-300">
|
||||
{{ t(getOAuthKey('emailPasswordDesc')) }}
|
||||
</p>
|
||||
<div class="mb-4">
|
||||
<label
|
||||
class="mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<Icon name="user" size="sm" class="text-blue-500" />
|
||||
{{ t(getOAuthKey('emailPasswordInputLabel')) }}
|
||||
<span
|
||||
v-if="parsedEmailPasswordCount > 1"
|
||||
class="rounded-full bg-blue-500 px-2 py-0.5 text-xs text-white"
|
||||
>
|
||||
{{ t('admin.accounts.oauth.keysCount', { count: parsedEmailPasswordCount }) }}
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
v-model="emailPasswordInput"
|
||||
rows="4"
|
||||
class="input w-full resize-y font-mono text-sm"
|
||||
:placeholder="t(getOAuthKey('emailPasswordPlaceholder'))"
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-blue-600 dark:text-blue-400">
|
||||
{{ t(getOAuthKey('emailPasswordHint')) }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="error"
|
||||
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-700 dark:bg-red-900/30"
|
||||
>
|
||||
<p class="whitespace-pre-line text-sm text-red-600 dark:text-red-400">
|
||||
{{ error }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary w-full"
|
||||
:disabled="loading || !emailPasswordInput.trim()"
|
||||
@click="handleAuthorizePassword"
|
||||
>
|
||||
<svg
|
||||
v-if="loading"
|
||||
class="-ml-1 mr-2 h-4 w-4 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<Icon v-else name="sparkles" size="sm" class="mr-2" />
|
||||
{{ loading ? t(getOAuthKey('validating')) : t(getOAuthKey('validateAndCreate')) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Codex auth.json / session credential batch import -->
|
||||
<div v-if="inputMethod === 'codex_session' || inputMethod === 'agent_identity'" class="space-y-4">
|
||||
<div
|
||||
@@ -836,6 +920,8 @@ interface Props {
|
||||
showAgentIdentityOption?: boolean
|
||||
showCodexPatOption?: boolean
|
||||
showSsoOption?: boolean
|
||||
/** Grok email----password login (admin; password never persisted). */
|
||||
showEmailPasswordOption?: boolean
|
||||
showManualOption?: boolean
|
||||
initialInputMethod?: AuthInputMethod
|
||||
platform?: AccountPlatform // Platform type for different UI/text
|
||||
@@ -860,6 +946,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
showAgentIdentityOption: false,
|
||||
showCodexPatOption: false,
|
||||
showSsoOption: false,
|
||||
showEmailPasswordOption: false,
|
||||
showManualOption: true,
|
||||
initialInputMethod: 'manual',
|
||||
platform: 'anthropic',
|
||||
@@ -877,6 +964,7 @@ const emit = defineEmits<{
|
||||
'import-codex-session': [content: string]
|
||||
'import-codex-pat': [accessToken: string]
|
||||
'import-sso': [content: string]
|
||||
'authorize-password': [emailPasswordInput: string]
|
||||
'update:inputMethod': [method: AuthInputMethod]
|
||||
}>()
|
||||
|
||||
@@ -922,6 +1010,7 @@ const sessionTokenInput = ref('')
|
||||
const codexSessionInput = ref('')
|
||||
const codexPATInput = ref('')
|
||||
const ssoCookieInput = ref('')
|
||||
const emailPasswordInput = ref('')
|
||||
const showHelpDialog = ref(false)
|
||||
const oauthState = ref('')
|
||||
const projectId = ref('')
|
||||
@@ -937,7 +1026,8 @@ const methodOptionCount = computed(() => [
|
||||
props.showCodexSessionImportOption,
|
||||
props.showAgentIdentityOption,
|
||||
props.showCodexPatOption,
|
||||
props.showSsoOption
|
||||
props.showSsoOption,
|
||||
props.showEmailPasswordOption
|
||||
].filter(Boolean).length)
|
||||
const showMethodSelection = computed(() => methodOptionCount.value > 1)
|
||||
|
||||
@@ -977,6 +1067,19 @@ const parsedSSOCount = computed(() => {
|
||||
.filter((item) => item).length
|
||||
})
|
||||
|
||||
const parsedEmailPasswordCount = computed(() => {
|
||||
return emailPasswordInput.value
|
||||
.split('\n')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item && item.includes('----')).length
|
||||
})
|
||||
|
||||
const handleAuthorizePassword = () => {
|
||||
if (emailPasswordInput.value.trim()) {
|
||||
emit('authorize-password', emailPasswordInput.value)
|
||||
}
|
||||
}
|
||||
|
||||
// Watchers
|
||||
watch(() => props.initialInputMethod, (newVal) => {
|
||||
inputMethod.value = newVal
|
||||
@@ -1081,6 +1184,7 @@ defineExpose({
|
||||
codexSession: codexSessionInput,
|
||||
codexPAT: codexPATInput,
|
||||
ssoCookie: ssoCookieInput,
|
||||
emailPassword: emailPasswordInput,
|
||||
inputMethod,
|
||||
reset: () => {
|
||||
authCodeInput.value = ''
|
||||
@@ -1092,6 +1196,7 @@ defineExpose({
|
||||
codexSessionInput.value = ''
|
||||
codexPATInput.value = ''
|
||||
ssoCookieInput.value = ''
|
||||
emailPasswordInput.value = ''
|
||||
inputMethod.value = props.initialInputMethod
|
||||
showHelpDialog.value = false
|
||||
}
|
||||
|
||||
@@ -23,9 +23,19 @@ describe('CreateAccountModal Grok account types', () => {
|
||||
expect(source).toContain('form.platform === \'grok\' && isOAuthFlow')
|
||||
})
|
||||
|
||||
it('validates and applies upstream config on all three Grok OAuth create paths', () => {
|
||||
// 授权码兑换 / RT 批量 / SSO 批量 3 处调用(定义为箭头函数,不计入)
|
||||
expect(source.match(/validateGrokOAuthUpstreamConfig\(\)/g)?.length).toBe(3)
|
||||
expect(source.match(/applyGrokOAuthUpstreamConfig\(credentials\)/g)?.length).toBe(3)
|
||||
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)
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user