feat: scope articles by brand
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
const CURRENT_BRAND_STORAGE_KEY = 'geo-rankly.current-brand-id'
|
||||
|
||||
export function readStoredCurrentBrandId(): number | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return null
|
||||
}
|
||||
|
||||
const raw = window.localStorage.getItem(CURRENT_BRAND_STORAGE_KEY)
|
||||
const value = Number(raw)
|
||||
return Number.isFinite(value) && value > 0 ? value : null
|
||||
}
|
||||
|
||||
export function writeStoredCurrentBrandId(brandId: number): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
window.localStorage.setItem(CURRENT_BRAND_STORAGE_KEY, String(brandId))
|
||||
}
|
||||
|
||||
export function clearStoredCurrentBrandId(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
window.localStorage.removeItem(CURRENT_BRAND_STORAGE_KEY)
|
||||
}
|
||||
Reference in New Issue
Block a user