122 lines
3.1 KiB
TypeScript
122 lines
3.1 KiB
TypeScript
import type { RedirectSsoOptions, SsoCodeExchangeOptions } from '@yunlefun/sso'
|
|
import {
|
|
readSsoClientId,
|
|
readSsoRedirectUri,
|
|
readSsoScope,
|
|
} from '@yunlefun/sso'
|
|
|
|
export interface YunlefunSsoConfigInput {
|
|
clientId?: string
|
|
cloudbaseEnv?: string
|
|
exchangeUrl?: string
|
|
nativeClientId?: string
|
|
nativeRedirectUri?: string
|
|
redirectUri?: string
|
|
redirectUris?: string | string[]
|
|
scope?: string
|
|
ssoOrigin?: string
|
|
}
|
|
|
|
export interface YunlefunSsoConfigContext {
|
|
currentOrigin: string
|
|
platform: 'native' | 'web'
|
|
}
|
|
|
|
export interface YunlefunSsoConfig {
|
|
cloudbaseEnv: string
|
|
exchange: SsoCodeExchangeOptions
|
|
redirect: RedirectSsoOptions
|
|
}
|
|
|
|
export type YunlefunSsoConfigResult
|
|
= | { ok: true, config: YunlefunSsoConfig }
|
|
| { ok: false, message: string }
|
|
|
|
const CLOUDBASE_ENV_RE = /^[a-z0-9][a-z0-9-]{0,63}$/i
|
|
const TRAILING_SLASH_RE = /\/+$/
|
|
|
|
export function resolveYunlefunSsoConfig(
|
|
input: YunlefunSsoConfigInput,
|
|
context: YunlefunSsoConfigContext,
|
|
): YunlefunSsoConfigResult {
|
|
const clientId = readSsoClientId(
|
|
context.platform === 'native' ? input.nativeClientId : input.clientId,
|
|
)
|
|
const cloudbaseEnv = normalizeValue(input.cloudbaseEnv)
|
|
const exchangeUrl = readSsoRedirectUri(input.exchangeUrl)
|
|
const webRedirectUris = readWebRedirectUris(input.redirectUris ?? input.redirectUri)
|
|
const redirectUri = context.platform === 'native'
|
|
? readSsoRedirectUri(input.nativeRedirectUri)
|
|
: webRedirectUris.find(uri => new URL(uri).origin === context.currentOrigin)
|
|
const scope = readSsoScope(input.scope)
|
|
const ssoOrigin = normalizeHttpsOrigin(input.ssoOrigin)
|
|
|
|
if (!clientId
|
|
|| !CLOUDBASE_ENV_RE.test(cloudbaseEnv)
|
|
|| !exchangeUrl
|
|
|| (context.platform === 'native' ? !redirectUri : webRedirectUris.length === 0)
|
|
|| scope.length === 0
|
|
|| !ssoOrigin) {
|
|
return {
|
|
ok: false,
|
|
message: '登录服务尚未完成 SSO v3 配置',
|
|
}
|
|
}
|
|
|
|
if (!redirectUri) {
|
|
return {
|
|
ok: false,
|
|
message: '当前域名尚未登记为登录回跳地址',
|
|
}
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
config: {
|
|
cloudbaseEnv,
|
|
exchange: { exchangeUrl },
|
|
redirect: {
|
|
clientId,
|
|
redirectUri,
|
|
scope,
|
|
ssoOrigin,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
function readWebRedirectUris(value: string | string[] | undefined): string[] {
|
|
const candidates = Array.isArray(value)
|
|
? value
|
|
: typeof value === 'string'
|
|
? value.split(/[\n,]/)
|
|
: []
|
|
|
|
return [...new Set(candidates
|
|
.map(candidate => readSsoRedirectUri(candidate.trim()))
|
|
.filter((candidate): candidate is string => Boolean(candidate)))]
|
|
}
|
|
|
|
function normalizeHttpsOrigin(value: string | undefined): string {
|
|
const normalized = normalizeValue(value).replace(TRAILING_SLASH_RE, '')
|
|
if (!normalized)
|
|
return ''
|
|
|
|
try {
|
|
const url = new URL(normalized)
|
|
return url.protocol === 'https:'
|
|
&& !url.username
|
|
&& !url.password
|
|
&& url.origin === normalized
|
|
? url.origin
|
|
: ''
|
|
}
|
|
catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
function normalizeValue(value: string | undefined): string {
|
|
return typeof value === 'string' ? value.trim() : ''
|
|
}
|