Files
what-to-eat/apps/web/app/utils/yunlefunSsoConfig.ts
T

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() : ''
}