mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 17:08:33 +08:00
feat(grok): 管理端补齐 SSO/密码授权前端入口
新增 sso-token 与 password API 封装及 composable 方法;buildCredentials 丢弃 sso/password 字段且不再强行固定 base_url,交由系统 CLI/API 模式选择主机。
This commit is contained in:
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user