mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08: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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,18 @@ import { useAppStore } from '@/stores/app'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
|
||||
export type AddMethod = 'oauth' | 'setup-token'
|
||||
export type AuthInputMethod = 'manual' | 'cookie' | 'refresh_token' | 'mobile_refresh_token' | 'session_token' | 'access_token' | 'codex_session' | 'agent_identity' | 'codex_pat' | 'sso_cookie'
|
||||
export type AuthInputMethod =
|
||||
| 'manual'
|
||||
| 'cookie'
|
||||
| 'refresh_token'
|
||||
| 'mobile_refresh_token'
|
||||
| 'session_token'
|
||||
| 'access_token'
|
||||
| 'codex_session'
|
||||
| 'agent_identity'
|
||||
| 'codex_pat'
|
||||
| 'sso_cookie'
|
||||
| 'email_password'
|
||||
|
||||
export interface OAuthState {
|
||||
authUrl: string
|
||||
|
||||
@@ -1009,6 +1009,17 @@ export default {
|
||||
ssoCookieLabel: 'Grok Web SSO Key',
|
||||
ssoCookiePlaceholder: 'One SSO key per line\nSupports multiple, one per line',
|
||||
ssoCookieHint: 'One SSO key per line. Multiple keys are imported with 3-way concurrency; expect about 90 seconds per batch. Use a matching-region proxy if needed.',
|
||||
emailPasswordAuth: 'Email + password',
|
||||
emailPasswordDesc:
|
||||
'Sign in with a Grok web email and password. The server uses the password only to obtain an ephemeral SSO cookie, then converts it to Build OAuth credentials. Neither the password nor raw SSO is stored on the account.',
|
||||
emailPasswordInputLabel: 'email----password',
|
||||
emailPasswordPlaceholder: 'user@example.com----your-password\nMultiple lines supported',
|
||||
emailPasswordHint:
|
||||
'Format: email----password (password may contain -). Requires YesCaptcha keys; use a matching-region proxy when needed.',
|
||||
pleaseEnterPassword: 'Please enter email----password (one per line)',
|
||||
pleaseEnterSSOToken: 'Please enter an SSO token',
|
||||
failedToValidateSSO: 'Failed to validate Grok SSO',
|
||||
failedToAuthorizePassword: 'Grok password authorization failed',
|
||||
convertingSSO: 'Converting...',
|
||||
convertSSOAndCreate: 'Convert & Create Account',
|
||||
validating: 'Validating...',
|
||||
|
||||
@@ -1062,6 +1062,16 @@ export default {
|
||||
ssoCookieLabel: 'Grok Web SSO Key',
|
||||
ssoCookiePlaceholder: '每行一个 SSO key\n支持多个,每行一个',
|
||||
ssoCookieHint: '每行一个 SSO key;多个 key 会 3 路并发导入,耗时约 90 秒 × 批次数,建议使用对应地区代理。',
|
||||
emailPasswordAuth: '邮箱密码登录',
|
||||
emailPasswordDesc:
|
||||
'使用 Grok 网页邮箱与密码登录。服务端仅用密码换取临时 SSO 再转 Build OAuth;密码与 raw SSO 均不会写入账号凭据。',
|
||||
emailPasswordInputLabel: '邮箱----密码',
|
||||
emailPasswordPlaceholder: 'user@example.com----your-password\n支持多个,每行一组',
|
||||
emailPasswordHint: '格式:email----password(密码可含 -)。需要配置 YesCaptcha 密钥;建议搭配代理。',
|
||||
pleaseEnterPassword: '请输入 email----password(每行一组)',
|
||||
pleaseEnterSSOToken: '请输入 SSO Token',
|
||||
failedToValidateSSO: '校验 Grok SSO 失败',
|
||||
failedToAuthorizePassword: 'Grok 密码授权失败',
|
||||
convertingSSO: '转换中...',
|
||||
convertSSOAndCreate: '转换并创建账号',
|
||||
validating: '验证中...',
|
||||
|
||||
Reference in New Issue
Block a user