611 lines
23 KiB
TypeScript
611 lines
23 KiB
TypeScript
import { createError } from 'h3'
|
||
import { createHmac, randomUUID } from 'node:crypto'
|
||
import { db } from '~/drizzle/db'
|
||
import { systemSettings } from '~/drizzle/schema'
|
||
import { asc } from 'drizzle-orm'
|
||
import {
|
||
isSafeAggregateOAuthUrl,
|
||
getAggregateOAuthLoginTypesOrDefault
|
||
} from './oauth-providers'
|
||
|
||
export interface OAuthUserInfo {
|
||
id: string
|
||
username: string
|
||
email?: string
|
||
name?: string
|
||
avatar?: string
|
||
}
|
||
|
||
export interface OAuthStrategy {
|
||
/**
|
||
* 获取授权跳转 URL
|
||
*/
|
||
getAuthorizeUrl(redirectUri: string, state: string): string
|
||
|
||
/**
|
||
* 使用 code 换取 access_token
|
||
*/
|
||
exchangeToken(code: string, redirectUri: string): Promise<string>
|
||
|
||
/**
|
||
* 获取用户信息
|
||
*/
|
||
getUserInfo(accessToken: string): Promise<OAuthUserInfo>
|
||
}
|
||
|
||
// GitHub 策略实现
|
||
const githubStrategy: OAuthStrategy = {
|
||
getAuthorizeUrl(redirectUri: string, state: string) {
|
||
const clientId = process.env.GITHUB_CLIENT_ID
|
||
if (!clientId)
|
||
throw createError({ statusCode: 500, message: 'GitHub Client ID not configured' })
|
||
|
||
return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=user:email&state=${encodeURIComponent(state)}`
|
||
},
|
||
|
||
async exchangeToken(code: string, redirectUri: string) {
|
||
const clientId = process.env.GITHUB_CLIENT_ID
|
||
const clientSecret = process.env.GITHUB_CLIENT_SECRET
|
||
|
||
if (!clientId || !clientSecret) {
|
||
throw createError({ statusCode: 500, message: 'GitHub config missing' })
|
||
}
|
||
|
||
try {
|
||
const tokenResponse = await $fetch<any>('https://github.com/login/oauth/access_token', {
|
||
method: 'POST',
|
||
body: {
|
||
client_id: clientId,
|
||
client_secret: clientSecret,
|
||
code,
|
||
redirect_uri: redirectUri
|
||
},
|
||
headers: { Accept: 'application/json' }
|
||
})
|
||
|
||
if (tokenResponse.error) {
|
||
console.error('GitHub Token Error:', tokenResponse.error)
|
||
throw new Error('授权失败,请重试')
|
||
}
|
||
|
||
if (!tokenResponse.access_token) {
|
||
console.error('GitHub Token Response missing access_token')
|
||
throw new Error('未能获取访问令牌')
|
||
}
|
||
return tokenResponse.access_token
|
||
} catch (e: any) {
|
||
console.error('GitHub token exchange failed', e.message || e)
|
||
const errorMessage = e.data?.error_description || e.message || '令牌请求失败'
|
||
throw new Error(errorMessage)
|
||
}
|
||
},
|
||
|
||
async getUserInfo(accessToken: string) {
|
||
try {
|
||
const userInfo = await $fetch<any>('https://api.github.com/user', {
|
||
headers: { Authorization: `Bearer ${accessToken}` }
|
||
})
|
||
|
||
return {
|
||
id: String(userInfo.id),
|
||
username: userInfo.login,
|
||
name: userInfo.name,
|
||
email: userInfo.email,
|
||
avatar: userInfo.avatar_url
|
||
}
|
||
} catch (e: any) {
|
||
console.error('GitHub user info failed', e)
|
||
throw new Error('获取用户信息失败')
|
||
}
|
||
}
|
||
}
|
||
|
||
// Casdoor 策略实现
|
||
const casdoorStrategy: OAuthStrategy = {
|
||
getAuthorizeUrl(redirectUri: string, state: string) {
|
||
const clientId = process.env.CASDOOR_CLIENT_ID
|
||
const endpoint = process.env.CASDOOR_ENDPOINT
|
||
if (!clientId || !endpoint)
|
||
throw createError({ statusCode: 500, message: 'Casdoor config missing' })
|
||
|
||
return `${endpoint}/login/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=read&state=${encodeURIComponent(state)}`
|
||
},
|
||
|
||
async exchangeToken(code: string, redirectUri: string) {
|
||
const clientId = process.env.CASDOOR_CLIENT_ID
|
||
const clientSecret = process.env.CASDOOR_CLIENT_SECRET
|
||
const endpoint = process.env.CASDOOR_ENDPOINT
|
||
|
||
if (!clientId || !clientSecret || !endpoint) {
|
||
throw createError({ statusCode: 500, message: 'Casdoor config missing' })
|
||
}
|
||
|
||
try {
|
||
const tokenResponse = await $fetch<any>(`${endpoint}/login/oauth/access_token`, {
|
||
method: 'POST',
|
||
body: {
|
||
client_id: clientId,
|
||
client_secret: clientSecret,
|
||
code,
|
||
grant_type: 'authorization_code',
|
||
redirect_uri: redirectUri
|
||
},
|
||
headers: { Accept: 'application/json' }
|
||
})
|
||
|
||
if (tokenResponse.error) {
|
||
console.error('Casdoor Token Error:', tokenResponse.error, tokenResponse.error_description)
|
||
throw new Error(tokenResponse.error_description || '授权失败,请重试')
|
||
}
|
||
|
||
if (!tokenResponse.access_token) {
|
||
console.error('Casdoor Token Response missing access_token')
|
||
throw new Error('未能获取访问令牌,请检查 Casdoor 配置')
|
||
}
|
||
return tokenResponse.access_token
|
||
} catch (e: any) {
|
||
console.error('Casdoor token exchange failed', e.message || e)
|
||
const errorMessage = e.data?.error_description || e.message || '令牌请求失败'
|
||
throw new Error(errorMessage)
|
||
}
|
||
},
|
||
|
||
async getUserInfo(accessToken: string) {
|
||
const endpoint = process.env.CASDOOR_ENDPOINT
|
||
if (!endpoint) throw createError({ statusCode: 500, message: 'Casdoor config missing' })
|
||
|
||
let userInfo: any = {}
|
||
try {
|
||
// Casdoor 用户信息端点通常是 /api/get-account 或 /api/userinfo
|
||
// 标准 OIDC 使用 /userinfo
|
||
try {
|
||
userInfo = await $fetch(`${endpoint}/api/userinfo`, {
|
||
headers: { Authorization: `Bearer ${accessToken}` }
|
||
})
|
||
} catch (e) {
|
||
// 尝试备用端点
|
||
userInfo = await $fetch(`${endpoint}/api/get-account`, {
|
||
headers: { Authorization: `Bearer ${accessToken}` }
|
||
})
|
||
}
|
||
|
||
return {
|
||
id: String(userInfo.id || userInfo.sub),
|
||
username: userInfo.name || userInfo.preferred_username || userInfo.username,
|
||
name: userInfo.displayName || userInfo.name,
|
||
email: userInfo.email,
|
||
avatar: userInfo.avatar
|
||
}
|
||
} catch (e: any) {
|
||
console.error('Casdoor user info failed', e)
|
||
throw new Error('获取用户信息失败')
|
||
}
|
||
}
|
||
}
|
||
|
||
// Google 策略实现
|
||
const googleStrategy: OAuthStrategy = {
|
||
getAuthorizeUrl(redirectUri: string, state: string) {
|
||
const clientId = process.env.GOOGLE_CLIENT_ID
|
||
if (!clientId)
|
||
throw createError({ statusCode: 500, message: 'Google Client ID not configured' })
|
||
|
||
return `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=openid%20email%20profile&state=${encodeURIComponent(state)}`
|
||
},
|
||
|
||
async exchangeToken(code: string, redirectUri: string) {
|
||
const clientId = process.env.GOOGLE_CLIENT_ID
|
||
const clientSecret = process.env.GOOGLE_CLIENT_SECRET
|
||
|
||
if (!clientId || !clientSecret) {
|
||
throw createError({ statusCode: 500, message: 'Google config missing' })
|
||
}
|
||
|
||
try {
|
||
const tokenResponse = await $fetch<any>('https://oauth2.googleapis.com/token', {
|
||
method: 'POST',
|
||
body: {
|
||
client_id: clientId,
|
||
client_secret: clientSecret,
|
||
code,
|
||
grant_type: 'authorization_code',
|
||
redirect_uri: redirectUri
|
||
},
|
||
headers: { Accept: 'application/json' }
|
||
})
|
||
|
||
if (tokenResponse.error) {
|
||
console.error('Google Token Error:', tokenResponse.error)
|
||
throw new Error('授权失败,请重试')
|
||
}
|
||
|
||
if (!tokenResponse.access_token) {
|
||
console.error('Google Token Response missing access_token')
|
||
throw new Error('未能获取访问令牌')
|
||
}
|
||
return tokenResponse.access_token
|
||
} catch (e: any) {
|
||
console.error('Google token exchange failed', e.message || e)
|
||
const errorMessage = e.data?.error_description || e.message || '令牌请求失败'
|
||
throw new Error(errorMessage)
|
||
}
|
||
},
|
||
|
||
async getUserInfo(accessToken: string) {
|
||
try {
|
||
const userInfo = await $fetch<any>('https://www.googleapis.com/oauth2/v3/userinfo', {
|
||
headers: { Authorization: `Bearer ${accessToken}` }
|
||
})
|
||
|
||
return {
|
||
id: userInfo.sub,
|
||
username: userInfo.email, // Google doesn't have a username, use email
|
||
name: userInfo.name,
|
||
email: userInfo.email,
|
||
avatar: userInfo.picture
|
||
}
|
||
} catch (e: any) {
|
||
console.error('Google user info failed', e)
|
||
throw new Error('获取用户信息失败')
|
||
}
|
||
}
|
||
}
|
||
|
||
// 策略注册表
|
||
const strategies: Record<string, OAuthStrategy> = {
|
||
github: githubStrategy,
|
||
casdoor: casdoorStrategy,
|
||
google: googleStrategy
|
||
}
|
||
|
||
async function loadAggregateSettings() {
|
||
const envAppId = process.env.AGGREGATE_OAUTH_APP_ID
|
||
const envAppKey = process.env.AGGREGATE_OAUTH_APP_KEY
|
||
const envLoginType = process.env.AGGREGATE_OAUTH_LOGIN_TYPE
|
||
const envEndpoint = process.env.AGGREGATE_OAUTH_ENDPOINT
|
||
const hasEnvConfig = !!(envAppId && envAppKey && envEndpoint)
|
||
|
||
let base: any = null
|
||
try {
|
||
const row = await db
|
||
.select({
|
||
aggregateOAuthEnabled: systemSettings.aggregateOAuthEnabled,
|
||
aggregateOAuthAppId: systemSettings.aggregateOAuthAppId,
|
||
aggregateOAuthAppKey: systemSettings.aggregateOAuthAppKey,
|
||
aggregateOAuthLoginType: systemSettings.aggregateOAuthLoginType,
|
||
aggregateOAuthEndpoint: systemSettings.aggregateOAuthEndpoint
|
||
})
|
||
.from(systemSettings)
|
||
.orderBy(asc(systemSettings.id))
|
||
.limit(1)
|
||
base = row[0] ?? null
|
||
} catch (e: any) {
|
||
console.warn(
|
||
'[AggregateOAuth][WARN] loadAggregateSettings 查询 DB 失败,回退只用 process.env 配置。' +
|
||
` DB 错误:${e?.message || String(e)}。 envAppId=${envAppId ? '已配置(len=' + String(envAppId).length + ')' : '空'} ` +
|
||
` envEndpoint=${envEndpoint ? '已配置(' + envEndpoint + ')' : '空'}`
|
||
)
|
||
}
|
||
|
||
const dbEnabled = base?.aggregateOAuthEnabled
|
||
const dbHasConfig = !!(
|
||
base?.aggregateOAuthAppId &&
|
||
base?.aggregateOAuthAppKey &&
|
||
base?.aggregateOAuthEndpoint
|
||
)
|
||
const enabled = Boolean(dbEnabled ?? hasEnvConfig)
|
||
const appId = (base?.aggregateOAuthAppId || envAppId || '') as string
|
||
const appKey = (base?.aggregateOAuthAppKey || envAppKey || '') as string
|
||
const loginType = ((base?.aggregateOAuthLoginType as string) || envLoginType || '') as string
|
||
const endpoint = ((base?.aggregateOAuthEndpoint as string) || envEndpoint || '') as string
|
||
|
||
const result: any = {
|
||
aggregateOAuthEnabled: false,
|
||
aggregateOAuthAppId: appId,
|
||
aggregateOAuthAppKey: appKey,
|
||
aggregateOAuthLoginType: loginType,
|
||
aggregateOAuthEndpoint: endpoint,
|
||
_dbHasConfig: dbHasConfig,
|
||
_hasEnvConfig: hasEnvConfig,
|
||
_hardDisabledReason:
|
||
'[hard-disabled] nuxt.config.ts public.oauth.aggregate 已硬编码 false,暂时关闭微信聚合登录;恢复时同时改 nuxt.config.ts 改回 !!process.env.AGGREGATE_OAUTH_APP_ID 以及删除此处的强制 false'
|
||
}
|
||
|
||
console.log(
|
||
'[AggregateOAuth] loadAggregateSettings -> ' +
|
||
` enabled=${result.aggregateOAuthEnabled}` +
|
||
` appId=${appId ? 'len=' + String(appId).length : 'EMPTY'}` +
|
||
` appKey=${appKey ? 'len=' + String(appKey).length : 'EMPTY'}` +
|
||
` loginType=${loginType || 'EMPTY'}` +
|
||
` endpoint=${endpoint || 'EMPTY'}` +
|
||
` (DB=${dbHasConfig ? '有' : '无'}配置, ENV=${hasEnvConfig ? '有' : '无'}配置, dbEnabled=${dbEnabled ?? '未设置->回退ENV'})`
|
||
)
|
||
|
||
return result
|
||
}
|
||
|
||
function normalizeEndpoint(endpoint: string): string {
|
||
return endpoint.replace(/\/+$/, '')
|
||
}
|
||
|
||
function aggregateSign(params: Record<string, string>, appKey: string): string {
|
||
const sorted = Object.keys(params)
|
||
.filter((k) => params[k] !== undefined && params[k] !== null && params[k] !== '')
|
||
.sort()
|
||
.map((k) => `${k}=${params[k]}`)
|
||
.join('&')
|
||
return createHmac('sha256', appKey).update(sorted).digest('hex')
|
||
}
|
||
|
||
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()
|
||
console.log(
|
||
'[AggregateOAuth][auxx][getAuthorizeUrl] ' +
|
||
`enabled=${settings?.aggregateOAuthEnabled}` +
|
||
` endpoint=${settings?.aggregateOAuthEndpoint || 'EMPTY'}` +
|
||
` endpoint_safe_ok=${isSafeAggregateOAuthUrl(settings?.aggregateOAuthEndpoint || '')}` +
|
||
` redirect_uri=${redirectUri}` +
|
||
` state_len=${String(state || '').length}`
|
||
)
|
||
if (!settings?.aggregateOAuthEnabled || !settings.aggregateOAuthAppId || !settings.aggregateOAuthAppKey || !settings.aggregateOAuthEndpoint) {
|
||
const hint = []
|
||
if (!settings?.aggregateOAuthEnabled) hint.push('aggregateOAuthEnabled=false')
|
||
if (!settings?.aggregateOAuthAppId) hint.push('APP_ID 空')
|
||
if (!settings?.aggregateOAuthAppKey) hint.push('APP_KEY 空')
|
||
if (!settings?.aggregateOAuthEndpoint) hint.push('ENDPOINT 空')
|
||
console.error('[AggregateOAuth][auxx][getAuthorizeUrl] 配置缺失:', hint.join(' | '))
|
||
throw createError({ statusCode: 500, message: '聚合登录未配置:' + hint.join(' / ') })
|
||
}
|
||
if (!isSafeAggregateOAuthUrl(settings.aggregateOAuthEndpoint)) {
|
||
console.error(
|
||
'[AggregateOAuth][auxx][getAuthorizeUrl] endpoint 不合法,' +
|
||
'isSafeAggregateOAuthUrl 返回 false。endpoint=' +
|
||
JSON.stringify(settings.aggregateOAuthEndpoint)
|
||
)
|
||
throw createError({
|
||
statusCode: 500,
|
||
message:
|
||
'聚合登录端点不安全或格式错误:endpoint 必须是 https://xxx 或内网 http://localhost,当前值=' +
|
||
settings.aggregateOAuthEndpoint
|
||
})
|
||
}
|
||
const loginTypes = getAggregateOAuthLoginTypesOrDefault(settings.aggregateOAuthLoginType)
|
||
const loginType = loginTypes[0] || 'qq'
|
||
const endpoint = normalizeEndpoint(settings.aggregateOAuthEndpoint)
|
||
const loginQueryObj: Record<string, string> = {
|
||
act: 'login',
|
||
appid: settings.aggregateOAuthAppId,
|
||
appkey: settings.aggregateOAuthAppKey,
|
||
type: loginType,
|
||
redirect_uri: redirectUri
|
||
}
|
||
const loginQuery = new URLSearchParams(loginQueryObj).toString()
|
||
const debugQueryNoKey = new URLSearchParams({ ...loginQueryObj, appkey: '***' }).toString()
|
||
console.log(
|
||
`[AggregateOAuth][auxx][getAuthorizeUrl] 请求 auxx Step1:GET ${endpoint}/connect.php?${debugQueryNoKey}`
|
||
)
|
||
try {
|
||
const resp: any = await $fetch(`${endpoint}/connect.php?${loginQuery}`, {
|
||
headers: { Accept: 'application/json' },
|
||
timeout: 15000
|
||
})
|
||
console.log(
|
||
'[AggregateOAuth][auxx][getAuthorizeUrl] auxx Step1 返回:' +
|
||
JSON.stringify({
|
||
code: resp?.code,
|
||
msg: resp?.msg,
|
||
has_url: !!resp?.url,
|
||
url_prefix: resp?.url ? String(resp.url).slice(0, 80) + '...' : null
|
||
})
|
||
)
|
||
if (resp.code !== 0 || !resp.url) {
|
||
throw new Error(resp.msg || `获取登录跳转地址失败(auxx code=${resp?.code ?? 'N/A'})`)
|
||
}
|
||
const sep = resp.url.includes('?') ? '&' : '?'
|
||
return `${resp.url}${sep}state=${encodeURIComponent(state)}`
|
||
} catch (e: any) {
|
||
const status = e?.response?.status || e?.status || 'N/A'
|
||
const body = e?.response?._data ? JSON.stringify(e.response._data).slice(0, 400) : e?.data ? JSON.stringify(e.data).slice(0, 400) : null
|
||
console.error(
|
||
'[AggregateOAuth][auxx][getAuthorizeUrl] fetch 异常:' +
|
||
` err=${e?.message || String(e)} status=${status} body=${body}`
|
||
)
|
||
throw new Error(e.message || '获取登录跳转地址失败(auxx 不可达或凭据错误)')
|
||
}
|
||
},
|
||
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) {
|
||
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 timestamp = String(Math.floor(Date.now() / 1000))
|
||
const nonce = randomUUID().replace(/-/g, '')
|
||
const baseParams: Record<string, string> = {
|
||
app_id: settings.aggregateOAuthAppId,
|
||
response_type: 'code',
|
||
redirect_uri: redirectUri,
|
||
state,
|
||
login_type: loginType,
|
||
timestamp,
|
||
nonce
|
||
}
|
||
const sign = aggregateSign(baseParams, settings.aggregateOAuthAppKey)
|
||
const query = new URLSearchParams({ ...baseParams, sign }).toString()
|
||
return `${endpoint}/oauth/authorize?${query}`
|
||
},
|
||
|
||
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 endpoint = normalizeEndpoint(settings.aggregateOAuthEndpoint)
|
||
const timestamp = String(Math.floor(Date.now() / 1000))
|
||
const nonce = randomUUID().replace(/-/g, '')
|
||
const baseParams: Record<string, string> = {
|
||
app_id: settings.aggregateOAuthAppId,
|
||
grant_type: 'authorization_code',
|
||
code,
|
||
redirect_uri: redirectUri,
|
||
timestamp,
|
||
nonce
|
||
}
|
||
const sign = aggregateSign(baseParams, settings.aggregateOAuthAppKey)
|
||
|
||
try {
|
||
const tokenResponse = await $fetch<any>(`${endpoint}/oauth/token`, {
|
||
method: 'POST',
|
||
body: { ...baseParams, sign },
|
||
headers: { Accept: 'application/json' }
|
||
})
|
||
if (tokenResponse.error) {
|
||
throw new Error(tokenResponse.error_description || tokenResponse.error || '令牌请求失败')
|
||
}
|
||
const accessToken = tokenResponse.access_token || tokenResponse.accessToken
|
||
if (!accessToken) {
|
||
throw new Error('未能获取访问令牌')
|
||
}
|
||
return accessToken
|
||
} catch (e: any) {
|
||
console.error('Aggregate OAuth token exchange failed', e)
|
||
throw new Error(e.message || '令牌请求失败')
|
||
}
|
||
},
|
||
|
||
async getUserInfo(accessToken: string) {
|
||
const settings = await loadAggregateSettings()
|
||
if (!settings?.aggregateOAuthAppId || !settings?.aggregateOAuthAppKey || !settings?.aggregateOAuthEndpoint) {
|
||
throw createError({ statusCode: 500, message: '聚合登录未配置' })
|
||
}
|
||
const endpoint = normalizeEndpoint(settings.aggregateOAuthEndpoint)
|
||
const timestamp = String(Math.floor(Date.now() / 1000))
|
||
const nonce = randomUUID().replace(/-/g, '')
|
||
const baseParams: Record<string, string> = {
|
||
app_id: settings.aggregateOAuthAppId,
|
||
access_token: accessToken,
|
||
timestamp,
|
||
nonce
|
||
}
|
||
const sign = aggregateSign(baseParams, settings.aggregateOAuthAppKey)
|
||
|
||
try {
|
||
const userInfo = await $fetch<any>(`${endpoint}/oauth/userinfo`, {
|
||
method: 'POST',
|
||
body: { ...baseParams, sign },
|
||
headers: { Accept: 'application/json' }
|
||
})
|
||
return {
|
||
id: String(userInfo.id || userInfo.openid || userInfo.sub || userInfo.userId || ''),
|
||
username: userInfo.username || userInfo.nickname || userInfo.name || String(userInfo.id || 'user'),
|
||
name: userInfo.name || userInfo.nickname || undefined,
|
||
email: userInfo.email || undefined,
|
||
avatar: userInfo.avatar || userInfo.avatar_url || userInfo.headimgurl || undefined
|
||
}
|
||
} catch (e: any) {
|
||
console.error('Aggregate OAuth user info failed', e)
|
||
throw new Error('获取用户信息失败')
|
||
}
|
||
}
|
||
}
|
||
|
||
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 => {
|
||
const strategy = strategies[provider]
|
||
if (!strategy) {
|
||
throw createError({ statusCode: 400, message: `Unknown provider: ${provider}` })
|
||
}
|
||
return strategy
|
||
}
|