601451f22b
Overlay ~150 upstream items onto our V8.12 base, preserving trade-pro
theme, registry tests, and design docs. Re-applied our patch set
(discovery/env_for/blocks.css/type-mapping/theme_color/frontend entries,
test_theme_pro_v3 mods) — all anchors survived upstream evolution.
New platform enhancement (fixes upgrade-introduced regression):
multilingual.site_locale now honors settings_json.locale, plus
PUT /api/i18n/sites/{id}/locale — standalone English sites get
<html lang='en'> under the V8.15 locale-rewrite pipeline.
compose: adopt V8.21.3 (image 8.21.3-dev.1, /api/ready probes, MySQL/S3
env) with project name pinned to siteforge-v8-10 (data volumes).
Tests: 356 passed / 11 failed — all 11 confirmed pre-existing in
pristine V8.21.3 on this host (10 upstream Windows env + our build test
now green after locale fix). Patch registry updated in plan doc.
Co-Authored-By: Claude Code <noreply@anthropic.com>
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)}}
|