feat(grok): 管理端补齐 SSO/密码授权前端入口

新增 sso-token 与 password API 封装及 composable 方法;buildCredentials
丢弃 sso/password 字段且不再强行固定 base_url,交由系统 CLI/API 模式选择主机。
This commit is contained in:
IanShaw027
2026-08-07 13:08:22 +08:00
parent 451abc3aa5
commit 0a28c99aae
3 changed files with 118 additions and 7 deletions
+40 -1
View File
@@ -170,4 +170,43 @@ export async function createFromSSO(payload: GrokSSOToOAuthRequest): Promise<Gro
return data
}
export default { generateAuthUrl, exchangeCode, refreshGrokToken, queryQuota, resetQuota, createFromSSO }
/** Validate a browser SSO cookie and convert to Build OAuth tokens (no raw SSO stored). */
export async function validateSSOToken(
ssoToken: string,
proxyId?: number | null
): Promise<GrokTokenInfo> {
const payload: Record<string, unknown> = { sso_token: ssoToken }
if (proxyId) payload.proxy_id = proxyId
const { data } = await apiClient.post<GrokTokenInfo>('/admin/grok/oauth/sso-token', payload)
return data
}
/**
* Password login → ephemeral SSO → Build OAuth.
* Password is only sent over the wire for this call; never persist it in credentials.
*/
export async function authorizePassword(
emailAndPassword: string,
proxyId?: number | null
): Promise<GrokTokenInfo> {
// Format: email----password (password may contain dashes).
const sep = '----'
const idx = emailAndPassword.indexOf(sep)
const email = (idx >= 0 ? emailAndPassword.slice(0, idx) : emailAndPassword).trim()
const password = idx >= 0 ? emailAndPassword.slice(idx + sep.length) : ''
const payload: Record<string, unknown> = { email, password }
if (proxyId) payload.proxy_id = proxyId
const { data } = await apiClient.post<GrokTokenInfo>('/admin/grok/oauth/password', payload)
return data
}
export default {
generateAuthUrl,
exchangeCode,
refreshGrokToken,
queryQuota,
resetQuota,
createFromSSO,
validateSSOToken,
authorizePassword,
}
@@ -55,7 +55,7 @@ describe('useGrokOAuth.exchangeAuthCode', () => {
})
describe('useGrokOAuth.buildCredentials', () => {
it('persists the Grok CLI subscription proxy for OAuth inference', () => {
it('builds OAuth credentials without forcing base_url or leaking sso/password', () => {
const oauth = useGrokOAuth()
const credentials = oauth.buildCredentials({
@@ -64,9 +64,20 @@ describe('useGrokOAuth.buildCredentials', () => {
expires_at: 1_900_000_000,
client_id: 'client-id',
scope: 'openid grok-cli:access',
email: 'grok@example.com'
})
email: 'grok@example.com',
password: 'super-secret',
sso_token: 'sso-cookie',
sso: 'sso-cookie',
'sso-rw': 'sso-cookie'
} as any)
expect(credentials.base_url).toBe('https://cli-chat-proxy.grok.com/v1')
expect(credentials.access_token).toBe('access-token')
expect(credentials.email).toBe('grok@example.com')
// System/CLI mode chooses the correct host; do not pin public API URL.
expect(credentials.base_url).toBeUndefined()
expect(credentials).not.toHaveProperty('password')
expect(credentials).not.toHaveProperty('sso_token')
expect(credentials).not.toHaveProperty('sso')
expect(credentials).not.toHaveProperty('sso-rw')
})
})
+63 -2
View File
@@ -113,6 +113,8 @@ export function useGrokOAuth() {
}
}
// Build account credentials for create/re-auth. Never persist raw SSO cookies
// or passwords: those exist only for the one-shot authorize API call.
const buildCredentials = (tokenInfo: GrokTokenInfo): Record<string, unknown> => {
const credentials: Record<string, unknown> = {
access_token: tokenInfo.access_token,
@@ -125,11 +127,16 @@ export function useGrokOAuth() {
team_id: tokenInfo.team_id,
subscription_tier: tokenInfo.subscription_tier,
entitlement_status: tokenInfo.entitlement_status,
base_url: 'https://cli-chat-proxy.grok.com/v1'
// Leave base_url unset so system/CLI mode can choose the correct host.
}
if (tokenInfo.refresh_token) credentials.refresh_token = tokenInfo.refresh_token
if (tokenInfo.id_token) credentials.id_token = tokenInfo.id_token
return Object.fromEntries(Object.entries(credentials).filter(([, value]) => value !== undefined && value !== ''))
const blocked = new Set(['sso_token', 'password', 'sso', 'sso-rw'])
return Object.fromEntries(
Object.entries(credentials).filter(
([key, value]) => !blocked.has(key) && value !== undefined && value !== ''
)
)
}
const buildExtraInfo = (tokenInfo: GrokTokenInfo): Record<string, unknown> => {
@@ -140,6 +147,58 @@ export function useGrokOAuth() {
return extra
}
const validateSSOToken = async (
ssoToken: string,
proxyId?: number | null
): Promise<GrokTokenInfo | null> => {
if (!ssoToken.trim()) {
error.value = t('admin.accounts.oauth.grok.pleaseEnterSSOToken', 'Please enter an SSO token')
return null
}
loading.value = true
error.value = ''
try {
return await adminAPI.grok.validateSSOToken(ssoToken.trim(), proxyId)
} catch (err: any) {
error.value = extractI18nErrorMessage(
err,
t,
'admin.accounts.oauth.grok.errors',
t('admin.accounts.oauth.grok.failedToValidateSSO', 'Failed to validate SSO token')
)
appStore.showError(error.value)
return null
} finally {
loading.value = false
}
}
const authorizePassword = async (
emailAndPassword: string,
proxyId?: number | null
): Promise<GrokTokenInfo | null> => {
if (!emailAndPassword.trim()) {
error.value = t('admin.accounts.oauth.grok.pleaseEnterPassword', 'Please enter email----password')
return null
}
loading.value = true
error.value = ''
try {
return await adminAPI.grok.authorizePassword(emailAndPassword, proxyId)
} catch (err: any) {
error.value = extractI18nErrorMessage(
err,
t,
'admin.accounts.oauth.grok.errors',
t('admin.accounts.oauth.grok.failedToAuthorizePassword', 'Password authorization failed')
)
appStore.showError(error.value)
return null
} finally {
loading.value = false
}
}
return {
authUrl,
sessionId,
@@ -150,6 +209,8 @@ export function useGrokOAuth() {
generateAuthUrl,
exchangeAuthCode,
validateRefreshToken,
validateSSOToken,
authorizePassword,
buildCredentials,
buildExtraInfo
}