feat(auth): aggregate OAuth 支持 auxx.cn 自定义规格(按 endpoint 自动分派)\n\n- oauth-strategies.ts: endpoint 命中 auxx.cn 时走新的 auxxStrategy\n - getAuthorizeUrl: GET {endpoint}/connect.php?act=login 拿 {url} 后转发(附 state)\n - exchangeToken: 直接 GET {endpoint}/connect.php?act=callback 拿 social_uid/access_token/nickname/faceimg,10 分钟内存缓存供 getUserInfo 读取\n - getUserInfo: 命中缓存返回 OAuthUserInfo(id=social_uid, username=nickname, avatar=faceimg)\n- 标准 OAuth2 /oauth/... 规格仍由 standardAggregateStrategy 保留,非 auxx 域名不受影响
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Successful in 2m11s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Failing after 8m40s
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Skipped

This commit is contained in:
2026-09-12 23:07:05 +08:00
parent 7b605351f4
commit 7f3e570d8a
+116 -1
View File
@@ -312,7 +312,98 @@ function aggregateSign(params: Record<string, string>, appKey: string): string {
return createHmac('sha256', appKey).update(sorted).digest('hex')
}
const aggregateStrategy: OAuthStrategy = {
const auxxUserInfoCache = new Map<string, any>()
function isAuxxEndpoint(endpoint: string): boolean {
return /(^|\.)auxx\.cn(\/|$)/i.test(endpoint)
}
const auxxStrategy: OAuthStrategy = {
async getAuthorizeUrl(redirectUri: string, state: string) {
const settings = await loadAggregateSettings()
if (!settings?.aggregateOAuthEnabled || !settings.aggregateOAuthAppId || !settings.aggregateOAuthAppKey || !settings.aggregateOAuthEndpoint) {
throw createError({ statusCode: 500, message: '聚合登录未配置' })
}
if (!isSafeAggregateOAuthUrl(settings.aggregateOAuthEndpoint)) {
throw createError({ statusCode: 500, message: '聚合登录端点不安全或格式错误' })
}
const loginTypes = getAggregateOAuthLoginTypesOrDefault(settings.aggregateOAuthLoginType)
const loginType = loginTypes[0] || 'qq'
const endpoint = normalizeEndpoint(settings.aggregateOAuthEndpoint)
const loginQuery = new URLSearchParams({
act: 'login',
appid: settings.aggregateOAuthAppId,
appkey: settings.aggregateOAuthAppKey,
type: loginType,
redirect_uri: redirectUri
}).toString()
try {
const resp: any = await $fetch(`${endpoint}/connect.php?${loginQuery}`, {
headers: { Accept: 'application/json' }
})
if (resp.code !== 0 || !resp.url) {
throw new Error(resp.msg || '获取登录跳转地址失败')
}
const sep = resp.url.includes('?') ? '&' : '?'
return `${resp.url}${sep}state=${encodeURIComponent(state)}`
} catch (e: any) {
console.error('Auxx OAuth authorize URL failed', e)
throw new Error(e.message || '获取登录跳转地址失败')
}
},
async exchangeToken(code: string, redirectUri: string) {
const settings = await loadAggregateSettings()
if (!settings?.aggregateOAuthEnabled || !settings.aggregateOAuthAppId || !settings.aggregateOAuthAppKey || !settings.aggregateOAuthEndpoint) {
throw createError({ statusCode: 500, message: '聚合登录未配置' })
}
const loginTypes = getAggregateOAuthLoginTypesOrDefault(settings.aggregateOAuthLoginType)
const loginType = loginTypes[0] || 'qq'
const endpoint = normalizeEndpoint(settings.aggregateOAuthEndpoint)
const cbQuery = new URLSearchParams({
act: 'callback',
appid: settings.aggregateOAuthAppId,
appkey: settings.aggregateOAuthAppKey,
type: loginType,
code
}).toString()
try {
const resp: any = await $fetch(`${endpoint}/connect.php?${cbQuery}`, {
headers: { Accept: 'application/json' }
})
if (resp.code !== 0 || !resp.access_token || !resp.social_uid) {
throw new Error(resp.msg || '回调换取用户信息失败')
}
const cacheKey = `${settings.aggregateOAuthAppId}:${resp.access_token}`
auxxUserInfoCache.set(cacheKey, resp)
setTimeout(() => auxxUserInfoCache.delete(cacheKey), 10 * 60 * 1000)
return String(resp.access_token)
} catch (e: any) {
console.error('Auxx OAuth token exchange failed', e)
throw new Error(e.message || '回调换取用户信息失败')
}
},
async getUserInfo(accessToken: string) {
const settings = await loadAggregateSettings()
const appId = settings?.aggregateOAuthAppId || ''
const cacheKey = `${appId}:${accessToken}`
let resp = auxxUserInfoCache.get(cacheKey)
if (!resp) {
resp = Array.from(auxxUserInfoCache.values()).find((r: any) => r.access_token === accessToken)
}
if (!resp || resp.code !== 0 || !resp.social_uid) {
throw new Error('获取用户信息失败')
}
return {
id: String(resp.social_uid),
username: resp.nickname || `wx_${String(resp.social_uid).slice(0, 8)}`,
name: resp.nickname || undefined,
email: undefined,
avatar: resp.faceimg || undefined
}
}
}
const standardAggregateStrategy: OAuthStrategy = {
async getAuthorizeUrl(redirectUri: string, state: string) {
const settings = await loadAggregateSettings()
if (!settings?.aggregateOAuthEnabled || !settings.aggregateOAuthAppId || !settings.aggregateOAuthAppKey || !settings.aggregateOAuthEndpoint) {
@@ -415,6 +506,30 @@ const aggregateStrategy: OAuthStrategy = {
}
}
const aggregateStrategy: OAuthStrategy = {
async getAuthorizeUrl(redirectUri: string, state: string) {
const settings = await loadAggregateSettings()
const endpoint = settings?.aggregateOAuthEndpoint || ''
return isAuxxEndpoint(endpoint)
? auxxStrategy.getAuthorizeUrl(redirectUri, state)
: standardAggregateStrategy.getAuthorizeUrl(redirectUri, state)
},
async exchangeToken(code: string, redirectUri: string) {
const settings = await loadAggregateSettings()
const endpoint = settings?.aggregateOAuthEndpoint || ''
return isAuxxEndpoint(endpoint)
? auxxStrategy.exchangeToken(code, redirectUri)
: standardAggregateStrategy.exchangeToken(code, redirectUri)
},
async getUserInfo(accessToken: string) {
const settings = await loadAggregateSettings()
const endpoint = settings?.aggregateOAuthEndpoint || ''
return isAuxxEndpoint(endpoint)
? auxxStrategy.getUserInfo(accessToken)
: standardAggregateStrategy.getUserInfo(accessToken)
}
}
strategies.aggregate = aggregateStrategy
export const getOAuthStrategy = (provider: string): OAuthStrategy => {