refactor(web): adopt modular Vue architecture and isolate bearer sessions
This commit is contained in:
1 parent
1b2b2b12ff
commit
3cd641a380
379 files changed
+19736
-8712
No files matched your search
@@ -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 }),
|
||||
}
|
||||
@@ -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`
|
||||
}
|
||||
@@ -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 } }),
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface InstalledPlugin {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
apiVersion: 1
|
||||
revision: string
|
||||
enabled: boolean
|
||||
}
|
||||
@@ -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 }),
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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' }),
|
||||
}
|
||||
@@ -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 }[]
|
||||
}
|
||||
@@ -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, '') }
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user