formatting and comments
This commit is contained in:
+120
-71
@@ -1,3 +1,12 @@
|
||||
/**
|
||||
* Same-origin API client and defensive wire-format normalizers.
|
||||
*
|
||||
* The backend is authoritative and may evolve response wrappers independently
|
||||
* from a deployed frontend. Normalizers accept those compatible wrappers while
|
||||
* producing strict UI models. Mutations are refused while offline and are never
|
||||
* queued for background replay, which avoids applying an old user's action
|
||||
* after logout or tenant switching.
|
||||
*/
|
||||
import type {
|
||||
AuthUser,
|
||||
ComponentSummary,
|
||||
@@ -13,7 +22,12 @@ export class ApiError extends Error {
|
||||
readonly code?: string
|
||||
readonly fieldErrors?: Record<string, string>
|
||||
|
||||
constructor(status: number, message: string, code?: string, fieldErrors?: Record<string, string>) {
|
||||
constructor(
|
||||
status: number,
|
||||
message: string,
|
||||
code?: string,
|
||||
fieldErrors?: Record<string, string>,
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
this.status = status
|
||||
@@ -36,7 +50,8 @@ export async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
throw new ApiError(0, '当前处于离线状态,操作没有提交;联网后请重试。', 'offline')
|
||||
}
|
||||
const headers = new Headers(init.headers)
|
||||
if (init.body != null && !headers.has('content-type')) headers.set('content-type', 'application/json')
|
||||
if (init.body != null && !headers.has('content-type'))
|
||||
headers.set('content-type', 'application/json')
|
||||
headers.set('accept', 'application/json')
|
||||
|
||||
const response = await fetch(path, {
|
||||
@@ -49,13 +64,13 @@ export async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
if (response.status === 401 && !path.startsWith('/api/v1/auth/')) {
|
||||
window.dispatchEvent(new Event('lxc:session-expired'))
|
||||
}
|
||||
const error = payload && typeof payload === 'object' ? payload as Record<string, unknown> : {}
|
||||
const error = payload && typeof payload === 'object' ? (payload as Record<string, unknown>) : {}
|
||||
throw new ApiError(
|
||||
response.status,
|
||||
String(error.message ?? error.error ?? `请求失败(HTTP ${response.status})`),
|
||||
typeof error.code === 'string' ? error.code : undefined,
|
||||
error.fieldErrors && typeof error.fieldErrors === 'object'
|
||||
? error.fieldErrors as Record<string, string>
|
||||
? (error.fieldErrors as Record<string, string>)
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
@@ -71,24 +86,29 @@ export function json(method: string, body?: unknown): RequestInit {
|
||||
|
||||
function object(value: unknown): Record<string, unknown> {
|
||||
return value != null && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
? (value as Record<string, unknown>)
|
||||
: {}
|
||||
}
|
||||
|
||||
export function normalizeSession(value: unknown): Session {
|
||||
const root = object(value)
|
||||
const candidate = root.user
|
||||
?? (root.authenticated === true || typeof root.id === 'string' || typeof root.username === 'string' ? root : null)
|
||||
const candidate =
|
||||
root.user ??
|
||||
(root.authenticated === true || typeof root.id === 'string' || typeof root.username === 'string'
|
||||
? root
|
||||
: null)
|
||||
const raw = object(candidate)
|
||||
const hasUser = typeof raw.id === 'string' || typeof raw.username === 'string'
|
||||
const user: AuthUser | null = hasUser ? {
|
||||
id: String(raw.id ?? ''),
|
||||
username: String(raw.username ?? ''),
|
||||
roomId: typeof raw.roomId === 'string' ? raw.roomId : undefined,
|
||||
displayName: typeof raw.displayName === 'string' ? raw.displayName : undefined,
|
||||
role: typeof raw.role === 'string' ? raw.role : 'user',
|
||||
totpEnabled: typeof raw.totpEnabled === 'boolean' ? raw.totpEnabled : undefined,
|
||||
} : null
|
||||
const user: AuthUser | null = hasUser
|
||||
? {
|
||||
id: String(raw.id ?? ''),
|
||||
username: String(raw.username ?? ''),
|
||||
roomId: typeof raw.roomId === 'string' ? raw.roomId : undefined,
|
||||
displayName: typeof raw.displayName === 'string' ? raw.displayName : undefined,
|
||||
role: typeof raw.role === 'string' ? raw.role : 'user',
|
||||
totpEnabled: typeof raw.totpEnabled === 'boolean' ? raw.totpEnabled : undefined,
|
||||
}
|
||||
: null
|
||||
return { user, setupRequired: root.setupRequired === true }
|
||||
}
|
||||
|
||||
@@ -98,19 +118,26 @@ export function normalizeEnrollment(value: unknown): TotpEnrollment {
|
||||
const enrollmentToken = root.enrollmentToken ?? totp.enrollmentToken ?? root.flowId ?? root.id
|
||||
return {
|
||||
enrollmentToken: String(enrollmentToken ?? ''),
|
||||
qrSvg: typeof totp.qrSvg === 'string' ? totp.qrSvg : typeof root.qrSvg === 'string' ? root.qrSvg : undefined,
|
||||
qrDataUrl: typeof totp.qrDataUrl === 'string'
|
||||
? totp.qrDataUrl
|
||||
: typeof totp.qrCodeDataUrl === 'string'
|
||||
? totp.qrCodeDataUrl
|
||||
: typeof root.qrDataUrl === 'string'
|
||||
? root.qrDataUrl
|
||||
qrSvg:
|
||||
typeof totp.qrSvg === 'string'
|
||||
? totp.qrSvg
|
||||
: typeof root.qrSvg === 'string'
|
||||
? root.qrSvg
|
||||
: undefined,
|
||||
qrDataUrl:
|
||||
typeof totp.qrDataUrl === 'string'
|
||||
? totp.qrDataUrl
|
||||
: typeof totp.qrCodeDataUrl === 'string'
|
||||
? totp.qrCodeDataUrl
|
||||
: typeof root.qrDataUrl === 'string'
|
||||
? root.qrDataUrl
|
||||
: undefined,
|
||||
otpauthUri:
|
||||
typeof totp.otpauthUri === 'string'
|
||||
? totp.otpauthUri
|
||||
: typeof root.otpauthUri === 'string'
|
||||
? root.otpauthUri
|
||||
: undefined,
|
||||
otpauthUri: typeof totp.otpauthUri === 'string'
|
||||
? totp.otpauthUri
|
||||
: typeof root.otpauthUri === 'string'
|
||||
? root.otpauthUri
|
||||
: undefined,
|
||||
manualKey: String(totp.manualKey ?? totp.secret ?? root.manualKey ?? root.secret ?? ''),
|
||||
expiresAt: typeof root.expiresAt === 'string' ? root.expiresAt : undefined,
|
||||
}
|
||||
@@ -119,24 +146,28 @@ export function normalizeEnrollment(value: unknown): TotpEnrollment {
|
||||
export function normalizeRecoveryCodes(value: unknown): string[] {
|
||||
const root = object(value)
|
||||
const codes = root.recoveryCodes ?? object(root.recovery).codes
|
||||
return Array.isArray(codes) ? codes.filter((code): code is string => typeof code === 'string') : []
|
||||
return Array.isArray(codes)
|
||||
? codes.filter((code): code is string => typeof code === 'string')
|
||||
: []
|
||||
}
|
||||
|
||||
export function normalizeComponents(value: unknown): ComponentSummary[] {
|
||||
const root = object(value)
|
||||
const list = Array.isArray(value) ? value : Array.isArray(root.components) ? root.components : []
|
||||
return list.map((entry) => {
|
||||
const item = object(entry)
|
||||
return {
|
||||
id: String(item.id ?? ''),
|
||||
publicId: String(item.publicId ?? item.public_id ?? item.id ?? ''),
|
||||
kind: String(item.kind ?? item.type ?? 'danmaku'),
|
||||
name: String(item.name ?? '弹幕姬'),
|
||||
enabled: typeof item.enabled === 'boolean' ? item.enabled : undefined,
|
||||
settings: item.settings as OverlaySettings | undefined,
|
||||
updatedAt: typeof item.updatedAt === 'string' ? item.updatedAt : undefined,
|
||||
}
|
||||
}).filter(item => item.id)
|
||||
return list
|
||||
.map(entry => {
|
||||
const item = object(entry)
|
||||
return {
|
||||
id: String(item.id ?? ''),
|
||||
publicId: String(item.publicId ?? item.public_id ?? item.id ?? ''),
|
||||
kind: String(item.kind ?? item.type ?? 'danmaku'),
|
||||
name: String(item.name ?? '弹幕姬'),
|
||||
enabled: typeof item.enabled === 'boolean' ? item.enabled : undefined,
|
||||
settings: item.settings as OverlaySettings | undefined,
|
||||
updatedAt: typeof item.updatedAt === 'string' ? item.updatedAt : undefined,
|
||||
}
|
||||
})
|
||||
.filter(item => item.id)
|
||||
}
|
||||
|
||||
export function normalizeSettings(value: unknown): OverlaySettings {
|
||||
@@ -147,50 +178,68 @@ export function normalizeSettings(value: unknown): OverlaySettings {
|
||||
export function normalizeSource(value: unknown): CookieCloudSource {
|
||||
const root = object(value)
|
||||
const source = object(root.source ?? root)
|
||||
const cookieCloud = object(source.cookieCloud ?? source.cookiecloud ?? root.cookieCloud ?? root.cookiecloud)
|
||||
const cookieCloud = object(
|
||||
source.cookieCloud ?? source.cookiecloud ?? root.cookieCloud ?? root.cookiecloud,
|
||||
)
|
||||
const status = object(source.status)
|
||||
return {
|
||||
roomId: String(source.roomId ?? root.roomId ?? ''),
|
||||
cookieCloud: {
|
||||
host: String(cookieCloud.host ?? ''),
|
||||
key: '',
|
||||
keyConfigured: cookieCloud.keyConfigured === true
|
||||
|| (typeof cookieCloud.key === 'string' && cookieCloud.key.length > 0),
|
||||
passwordConfigured: cookieCloud.passwordConfigured === true
|
||||
|| cookieCloud.configured === true
|
||||
|| (typeof cookieCloud.password === 'string' && cookieCloud.password.length > 0),
|
||||
keyConfigured:
|
||||
cookieCloud.keyConfigured === true ||
|
||||
(typeof cookieCloud.key === 'string' && cookieCloud.key.length > 0),
|
||||
passwordConfigured:
|
||||
cookieCloud.passwordConfigured === true ||
|
||||
cookieCloud.configured === true ||
|
||||
(typeof cookieCloud.password === 'string' && cookieCloud.password.length > 0),
|
||||
},
|
||||
connected: typeof source.connected === 'boolean'
|
||||
? source.connected
|
||||
: typeof status.connected === 'boolean'
|
||||
? status.connected
|
||||
: undefined,
|
||||
detail: typeof source.detail === 'string'
|
||||
? source.detail
|
||||
: typeof status.detail === 'string'
|
||||
? status.detail
|
||||
: undefined,
|
||||
connected:
|
||||
typeof source.connected === 'boolean'
|
||||
? source.connected
|
||||
: typeof status.connected === 'boolean'
|
||||
? status.connected
|
||||
: undefined,
|
||||
detail:
|
||||
typeof source.detail === 'string'
|
||||
? source.detail
|
||||
: typeof status.detail === 'string'
|
||||
? status.detail
|
||||
: undefined,
|
||||
updatedAt: typeof source.updatedAt === 'string' ? source.updatedAt : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeInvitations(value: unknown): Invitation[] {
|
||||
const root = object(value)
|
||||
const list = Array.isArray(value) ? value : Array.isArray(root.invitations) ? root.invitations : []
|
||||
return list.map((entry) => {
|
||||
const item = object(entry)
|
||||
return {
|
||||
id: String(item.id ?? ''),
|
||||
code: typeof item.code === 'string' ? item.code : undefined,
|
||||
codePrefix: typeof item.codePrefix === 'string' ? item.codePrefix : undefined,
|
||||
roomId: String(item.roomId ?? ''),
|
||||
createdBy: typeof item.createdBy === 'string' ? item.createdBy : undefined,
|
||||
createdAt: typeof item.createdAt === 'string' ? item.createdAt : undefined,
|
||||
expiresAt: typeof item.expiresAt === 'string' ? item.expiresAt : undefined,
|
||||
consumedAt: typeof item.consumedAt === 'string' || item.consumedAt === null ? item.consumedAt : undefined,
|
||||
revokedAt: typeof item.revokedAt === 'string' || item.revokedAt === null ? item.revokedAt : undefined,
|
||||
}
|
||||
}).filter(item => item.id)
|
||||
const list = Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(root.invitations)
|
||||
? root.invitations
|
||||
: []
|
||||
return list
|
||||
.map(entry => {
|
||||
const item = object(entry)
|
||||
return {
|
||||
id: String(item.id ?? ''),
|
||||
code: typeof item.code === 'string' ? item.code : undefined,
|
||||
codePrefix: typeof item.codePrefix === 'string' ? item.codePrefix : undefined,
|
||||
roomId: String(item.roomId ?? ''),
|
||||
createdBy: typeof item.createdBy === 'string' ? item.createdBy : undefined,
|
||||
createdAt: typeof item.createdAt === 'string' ? item.createdAt : undefined,
|
||||
expiresAt: typeof item.expiresAt === 'string' ? item.expiresAt : undefined,
|
||||
consumedAt:
|
||||
typeof item.consumedAt === 'string' || item.consumedAt === null
|
||||
? item.consumedAt
|
||||
: undefined,
|
||||
revokedAt:
|
||||
typeof item.revokedAt === 'string' || item.revokedAt === null
|
||||
? item.revokedAt
|
||||
: undefined,
|
||||
}
|
||||
})
|
||||
.filter(item => item.id)
|
||||
}
|
||||
|
||||
export function errorMessage(error: unknown, fallback = '操作失败,请稍后再试'): string {
|
||||
|
||||
Reference in New Issue
Block a user