feat: scope articles by brand

This commit is contained in:
2026-05-20 15:37:25 +08:00
parent 5fb9d0b0dd
commit dd082e2ed1
72 changed files with 3213 additions and 432 deletions
+61
View File
@@ -148,6 +148,7 @@ import {
readStoredSession,
setStoredTokens,
} from './session'
import { readStoredCurrentBrandId } from './current-brand'
const rawBaseURL = import.meta.env.VITE_API_BASE_URL ?? ''
const baseURL = normalizeApiBaseURL(rawBaseURL)
@@ -312,6 +313,64 @@ export const apiClient = createApiClient({
},
})
const currentBrandScopedPathPatterns = [
/^\/api\/tenant\/workspace\/(?:overview|recent-articles)(?:\/|$)/,
/^\/api\/tenant\/articles(?:\/|$)/,
/^\/api\/tenant\/templates\/[^/]+\/(?:drafts|generate)(?:\/|$)/,
/^\/api\/tenant\/kol\/subscription-prompts\/[^/]+\/generate(?:\/|$)/,
/^\/api\/tenant\/schedules(?:\/|$)/,
/^\/api\/tenant\/instant-tasks(?:\/|$)/,
/^\/api\/tenant\/publish-jobs(?:\/|$)/,
/^\/api\/tenant\/publish-tasks\/[^/]+\/retry(?:\/|$)/,
]
function shouldAttachCurrentBrandHeader(url: unknown): boolean {
if (typeof url !== 'string') {
return false
}
const pathname = normalizeRequestPath(url)
return currentBrandScopedPathPatterns.some((pattern) => pattern.test(pathname))
}
function normalizeRequestPath(url: string): string {
if (/^https?:\/\//i.test(url)) {
try {
return new URL(url).pathname
} catch {
return url
}
}
return url.split('?')[0] || url
}
function buildCurrentBrandHeaders(path: string): Headers | undefined {
if (!shouldAttachCurrentBrandHeader(path)) {
return undefined
}
const brandId = readStoredCurrentBrandId()
if (!brandId) {
return undefined
}
const headers = new Headers()
headers.set('X-Brand-ID', String(brandId))
return headers
}
apiClient.raw.interceptors.request.use((config) => {
if (!shouldAttachCurrentBrandHeader(config.url)) {
return config
}
const brandId = readStoredCurrentBrandId()
if (!brandId) {
return config
}
config.headers = config.headers ?? {}
;(config.headers as Record<string, string>)['X-Brand-ID'] = String(brandId)
return config
})
const membershipBlockedErrors = new Set([
'user_disabled',
'trial_plan_expired',
@@ -910,6 +969,8 @@ async function subscribeSSE<TEvent extends SSEEventShape>(
const requestHeaders = new Headers(init.headers)
requestHeaders.set('Accept', 'text/event-stream')
requestHeaders.set('Authorization', `Bearer ${await getFreshStoredAccessToken()}`)
const currentBrandHeaders = buildCurrentBrandHeaders(path)
currentBrandHeaders?.forEach((value, key) => requestHeaders.set(key, value))
let response = await fetch(`${baseURL}${path}`, {
...init,