54 lines
3.1 KiB
TypeScript
54 lines
3.1 KiB
TypeScript
|
|
/** SiteForge session/API boundary. Do not use Art demo login tokens as SiteForge credentials. */
|
||
|
|
export interface SiteRef { id:string; name:string; domain?:string }
|
||
|
|
export interface SiteForgeCapabilities { role:string; view:boolean; edit:boolean; review:boolean; deploy:boolean; manage_members:boolean; create_site:boolean; manage_users:boolean }
|
||
|
|
export interface SiteForgeBootstrap {
|
||
|
|
version:string; user:string; superadmin:boolean; sites:SiteRef[]; selected_site_id:string|null;
|
||
|
|
capabilities:SiteForgeCapabilities;
|
||
|
|
navigation:Array<{key:string;title:string;path:string;requires_site:boolean}>
|
||
|
|
}
|
||
|
|
let csrfCache:string|undefined
|
||
|
|
const key='siteforge:art-selected-site'
|
||
|
|
|
||
|
|
export class SiteForgeError extends Error {
|
||
|
|
constructor(message:string,readonly status:number){super(message);this.name='SiteForgeError'}
|
||
|
|
}
|
||
|
|
export async function sfApi<T>(path:string, init:RequestInit={}):Promise<T>{
|
||
|
|
if(!path.startsWith('/api/') || path.startsWith('//'))throw new Error('仅允许调用本站 SiteForge API')
|
||
|
|
const method=(init.method||'GET').toUpperCase()
|
||
|
|
const headers=new Headers(init.headers)
|
||
|
|
if(!['GET','HEAD'].includes(method)){
|
||
|
|
if(!csrfCache){const me=await sfApi<{csrf:string}>('/api/auth/me');csrfCache=me.csrf}
|
||
|
|
headers.set('X-SiteForge-CSRF',csrfCache)
|
||
|
|
}
|
||
|
|
const res=await fetch(path,{...init,method,headers,credentials:'same-origin',redirect:'follow'})
|
||
|
|
if(res.status===401){csrfCache=undefined;throw new SiteForgeError('SiteForge 登录已过期,请从统一登录页重新登录',401)}
|
||
|
|
const contentType=res.headers.get('content-type')||''
|
||
|
|
if(!contentType.includes('application/json'))throw new SiteForgeError('接口返回了非 JSON 响应,请检查同域 API 代理配置',502)
|
||
|
|
const data=await res.json()
|
||
|
|
if(!res.ok){
|
||
|
|
const detail=typeof data?.detail==='string'?data.detail:JSON.stringify(data?.detail||'请求失败')
|
||
|
|
throw new SiteForgeError(detail,res.status)
|
||
|
|
}
|
||
|
|
return data as T
|
||
|
|
}
|
||
|
|
export async function session(){const me=await sfApi<{csrf:string;user:string}>('/api/auth/me');csrfCache=me.csrf;return me}
|
||
|
|
export function preferredSite<T extends SiteRef>(sites:T[]):string{
|
||
|
|
const params=new URLSearchParams(window.location.search)
|
||
|
|
const proposed=params.get('site') || sessionStorage.getItem(key) || ''
|
||
|
|
const chosen=sites.find(s=>s.id===proposed) || sites[0]
|
||
|
|
if(chosen)sessionStorage.setItem(key,chosen.id)
|
||
|
|
return chosen?.id||''
|
||
|
|
}
|
||
|
|
export function rememberSite(siteId:string){if(siteId)sessionStorage.setItem(key,siteId)}
|
||
|
|
export async function bootstrap():Promise<SiteForgeBootstrap>{
|
||
|
|
const preferred=sessionStorage.getItem(key)||''
|
||
|
|
try {
|
||
|
|
return await sfApi<SiteForgeBootstrap>('/api/console/bootstrap'+(preferred?'?site_id='+encodeURIComponent(preferred):''))
|
||
|
|
} catch(e){
|
||
|
|
if(e instanceof SiteForgeError && e.status===403){sessionStorage.removeItem(key);return sfApi<SiteForgeBootstrap>('/api/console/bootstrap')}
|
||
|
|
throw e
|
||
|
|
}
|
||
|
|
}
|
||
|
|
export function postJson(body:unknown):RequestInit{return {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}}
|
||
|
|
export function patchJson(body:unknown):RequestInit{return {method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}}
|