refactor(web): adopt modular Vue architecture and isolate bearer sessions

This commit is contained in:
eezd committed 2026-10-25 20:47:32 +08:00
1 parent 1b2b2b12ff
commit 3cd641a380
379 files changed
+19736 -8712

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
import { requestJSON } from '@/http/axios'
export interface LoginResult { token: string, expiresAt: string }
export interface Identity { permissions: string[], expiresAt: string }
export interface Captcha { captchaId: string, imageDataUrl: string, expiresAt: string }
export interface ResourceTicket { url: string, expiresAt: string }
export type TicketRequest = { kind: 'images' | 'fonts' | 'events' } | { kind: 'plugin', pluginId: string, revision: string }
export const authApi = {
state: () => requestJSON<{ setUp: boolean }>('GET', '/auth/state', { auth: false, errorOwner: 'caller' }),
captcha: (replaceId?: string) => requestJSON<Captcha>('POST', '/auth/captcha', { auth: false, json: { replaceId }, errorOwner: 'caller' }),
setup: (password: string) => requestJSON<LoginResult>('POST', '/auth/setup', { auth: false, json: { password }, errorOwner: 'caller' }),
login: (password: string, captchaId: string, captchaCode: string) => requestJSON<LoginResult>('POST', '/auth/login', { auth: false, json: { password, captchaId, captchaCode }, errorOwner: 'caller' }),
me: () => requestJSON<Identity>('GET', '/auth/me', { errorOwner: 'caller' }),
logout: () => requestJSON<null>('POST', '/auth/logout'),
ticket: (data: TicketRequest) => requestJSON<ResourceTicket>('POST', '/auth/tickets', { json: data }),
}
+40
View File
@@ -0,0 +1,40 @@
import { pinia } from '@/pinia'
import { useUserStore } from '@/pinia/stores/user'
import { authApi } from './index'
let version = -1
let imageRoot = ''
let fontRoot = ''
let pending: Promise<void> | undefined
export async function prepareResources() {
const user = useUserStore(pinia)
const current = user.getSessionVersion()
if (version !== current) {
version = current; imageRoot = ''; fontRoot = ''; pending = undefined
}
if (imageRoot && fontRoot) return
if (!pending) {
const request = Promise.all([authApi.ticket({ kind: 'images' }), authApi.ticket({ kind: 'fonts' })]).then(([images, fonts]) => {
user.assertSession(current)
imageRoot = images.url
fontRoot = fonts.url
})
pending = request
void request.finally(() => {
if (pending === request) pending = undefined
}).catch(() => { /* 由等待初始化的调用方处理失败。 */ })
}
await pending
user.assertSession(current)
}
export function imageResourceRoot(): string {
if (!imageRoot || version !== useUserStore(pinia).getSessionVersion()) throw new Error('图片资源认证尚未就绪')
return imageRoot
}
export function fontResourceRoot(): string {
if (!fontRoot || version !== useUserStore(pinia).getSessionVersion()) throw new Error('字体资源认证尚未就绪')
return fontRoot
}
export async function pluginResourceURL(pluginId: string, revision: string): Promise<string> {
return `${(await authApi.ticket({ kind: 'plugin', pluginId, revision })).url}main.js`
}
+15
View File
@@ -0,0 +1,15 @@
import type { InstalledPlugin } from './type'
import { requestJSON } from '@/http/axios'
export const pluginsApi = {
plugins: async () => (await requestJSON<{ plugins: InstalledPlugin[] }>('GET', '/plugins', { errorOwner: 'caller' })).plugins,
installPlugin: (file: File) => {
const body = new FormData()
body.append('file', file)
return requestJSON<InstalledPlugin>('POST', '/plugins', { errorOwner: 'caller', body })
},
togglePlugin: (id: string, enabled: boolean) => requestJSON<InstalledPlugin>('PATCH', `/plugins/${encodeURIComponent(id)}`, { errorOwner: 'caller', json: { enabled } }),
uninstallPlugin: (id: string) => requestJSON('DELETE', `/plugins/${encodeURIComponent(id)}`, { errorOwner: 'caller' }),
pluginData: (id: string) => requestJSON<{ data: unknown }>('GET', `/plugins/${encodeURIComponent(id)}/data`, { errorOwner: 'caller' }),
savePluginData: (id: string, data: unknown) => requestJSON('PUT', `/plugins/${encodeURIComponent(id)}/data`, { errorOwner: 'caller', json: { data } }),
}
+8
View File
@@ -0,0 +1,8 @@
export interface InstalledPlugin {
id: string
name: string
version: string
apiVersion: 1
revision: string
enabled: boolean
}
+16
View File
@@ -0,0 +1,16 @@
import type { CustomFont, Theme } from './type'
import { requestJSON } from '@/http/axios'
export const preferencesApi = {
settings: () => requestJSON<Record<string, unknown>>('GET', '/settings', { errorOwner: 'caller' }),
fonts: async () => (await requestJSON<{ fonts: CustomFont[] }>('GET', '/fonts', { errorOwner: 'caller' })).fonts,
importFont: (file: File) => {
const body = new FormData()
body.append('file', file)
return requestJSON<CustomFont>('POST', '/fonts', { errorOwner: 'caller', body })
},
deleteFont: (id: string) => requestJSON('DELETE', `/fonts/${encodeURIComponent(id)}`, { errorOwner: 'caller' }),
themes: async () => (await requestJSON<{ themes: Theme[] }>('GET', '/themes', { errorOwner: 'caller' })).themes,
theme: (id: string) => requestJSON<Theme & { css: string }>('GET', `/themes/${encodeURIComponent(id)}`, { errorOwner: 'caller' }),
patchSettings: (patch: Record<string, unknown>) => requestJSON<Record<string, unknown>>('PATCH', '/settings', { errorOwner: 'caller', json: patch }),
}
+11
View File
@@ -0,0 +1,11 @@
export interface Theme {
id: string
name: string
version: string
}
export interface CustomFont {
id: string
name: string
format: 'ttf' | 'otf' | 'woff' | 'woff2'
}
+9
View File
@@ -0,0 +1,9 @@
import type { TrashItem, TrashPurgeResult } from './type'
import { requestJSON } from '@/http/axios'
export const trashApi = {
clearTrash: () => requestJSON<TrashPurgeResult>('DELETE', '/trash', { errorOwner: 'caller' }),
trash: async () => (await requestJSON<{ items: TrashItem[] }>('GET', '/trash', { errorOwner: 'caller' })).items,
restore: (id: string) => requestJSON<TrashItem>('POST', `/trash/${id}/restore`, { errorOwner: 'caller' }),
purge: (id: string) => requestJSON('DELETE', `/trash/${id}`, { errorOwner: 'caller' }),
}
+11
View File
@@ -0,0 +1,11 @@
export interface TrashItem {
id: string
path: string
dir: boolean
deletedAt: number
}
export interface TrashPurgeResult {
deleted: string[]
failed: { id: string, error: string }[]
}
+16
View File
@@ -0,0 +1,16 @@
import type { Entry } from './type'
import { encodePath, requestJSON, requestRaw } from '@/http/axios'
import { pinia } from '@/pinia'
import { useUserStore } from '@/pinia/stores/user'
export const vaultApi = {
tree: async () => (await requestJSON<{ entries: Entry[] }>('GET', '/tree', { errorOwner: 'caller' })).entries,
async readFile(path: string): Promise<{ content: string, rev: string }> {
const user = useUserStore(pinia)
const version = user.getSessionVersion()
const res = await requestRaw('GET', `/files/${encodePath(path)}`, { errorOwner: 'caller' })
const content = await res.text()
user.assertSession(version)
return { content, rev: (res.headers.get('ETag') ?? '').replace(/"/g, '') }
},
}
+11
View File
@@ -0,0 +1,11 @@
import type { IconName } from '@@/utils/icons'
export type TreeIcon = { kind: 'emoji', value: string } | { kind: 'builtin', value: IconName }
export interface Entry {
path: string
dir: boolean
size: number
mtime: number
icon?: TreeIcon
}