Files
FileShare/web/src/api/admin.ts
T
SKYMirror 7f060dd0e4 26.9(安全审计修复版)
Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务:

- 安全审计全部修复(docs/security-audit-2026-09-05.md):
  bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、
  全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、
  管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、
  会话默认 7 天、限流缓存故障降级、robots.txt 端点等
- 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固
- Redis 支持库号(FCB_REDIS_DB / redis://…/db URL)
- 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、
  提码 5 位起、chunk 32MiB 上限、admin 审计动作等)

验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
2026-09-05 04:22:41 +08:00

158 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 管理后台 API(对齐 t3 契约:login/verify/logout/file list/update/delete/batch-delete/config/audit */
import { request } from './http'
import { paths } from './paths'
import type {
AdminConfig,
AdminFileListResult,
AdminFileUpdatePayload,
AdminLoginResult,
AuditListResult,
} from './types'
import { pick } from '@/utils/format'
/** 管理员登录(密码错误 → 401 ApiError */
export function adminLogin(password: string): Promise<AdminLoginResult> {
return request<AdminLoginResult>(paths.adminLogin, { method: 'POST', json: { password } })
}
export function adminVerify(): Promise<Record<string, unknown>> {
return request<Record<string, unknown>>(paths.adminVerify)
}
export async function adminLogout(): Promise<void> {
await request<void>(paths.adminLogout, { method: 'POST' })
}
export interface AdminFileListQuery {
page: number
size: number
keyword?: string
}
/** 文件列表(宽松解析:兼容 data 字段或 list 字段、total 顶层) */
export async function adminFileList(q: AdminFileListQuery): Promise<AdminFileListResult> {
const raw = await request<Record<string, unknown>>(paths.adminFileList, {
query: { page: q.page, size: q.size, keyword: q.keyword || undefined },
})
const rows = (pick<unknown[]>(raw, ['data', 'list', 'items', 'files']) ?? []) as Record<string, unknown>[]
const total = Number(pick<number>(raw, ['total', 'count']) ?? rows.length)
return {
page: Number(pick<number>(raw, ['page']) ?? q.page),
size: Number(pick<number>(raw, ['size']) ?? q.size),
total,
data: rows.map((r) => ({
id: Number(pick<number>(r, ['id']) ?? 0),
code: String(pick<string>(r, ['code']) ?? ''),
name: String(pick<string>(r, ['name']) ?? `${pick<string>(r, ['prefix']) ?? ''}${pick<string>(r, ['suffix']) ?? ''}`),
suffix: String(pick<string>(r, ['suffix']) ?? ''),
size: Number(pick<number>(r, ['size']) ?? 0),
isText: Boolean(pick<boolean>(r, ['isText', 'is_text']) ?? false),
expiredAt: (pick<string>(r, ['expiredAt', 'expired_at', 'expires_at']) as string) ?? null,
expiredCount: (pick<number>(r, ['expiredCount', 'expired_count']) as number) ?? null,
usedCount: Number(pick<number>(r, ['usedCount', 'used_count']) ?? 0),
createdAt: (pick<string>(r, ['createdAt', 'created_at']) as string) ?? null,
isExpired: Boolean(pick<boolean>(r, ['isExpired', 'is_expired']) ?? false),
})),
}
}
/** 删除单个文件 */
export async function adminFileDelete(id: number): Promise<void> {
await request<void>(paths.adminFileDelete, { method: 'DELETE', json: { id } })
}
/** 批量删除(契约:POST /admin/file/batch-deletebody {ids:number[]} */
export async function adminFileBatchDelete(ids: number[]): Promise<void> {
await request<void>(paths.adminFileBatchDelete, { method: 'POST', json: { ids } })
}
/** 更新分享(code/过期时间/次数;go-api 定稿 PATCH /admin/file/update */
export async function adminFileUpdate(payload: AdminFileUpdatePayload): Promise<void> {
await request<void>(paths.adminFileUpdate, { method: 'PATCH', json: payload })
}
/** 读取管理端配置(KV 全量,含 site_name/logo_url/favicon_url */
export async function adminConfigGet(): Promise<AdminConfig> {
const raw = await request<unknown>(paths.adminConfigGet)
// 兼容:后端可能直接返回 KV 对象,或包一层 config/data
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
const obj = raw as Record<string, unknown>
const inner = (pick<Record<string, unknown>>(obj, ['config', 'data', 'settings']) ?? obj) as Record<string, unknown>
return inner
}
return {}
}
/** 更新管理端配置(PATCH 部分字段 JSON;提交后全站生效,含 site_name/logo_url/favicon_url */
export async function adminConfigUpdate(patch: Record<string, unknown>): Promise<void> {
await request<void>(paths.adminConfigUpdate, { method: 'PATCH', json: patch })
}
/** v3 存储引擎热切换:成功返回当前引擎名;失败(503)服务端保持原引擎并抛 ApiError */
export async function adminStorageSwitch(engine: 'local' | 's3' | 'webdav'): Promise<string> {
const data = await request<{ ok: boolean; engine: string }>(paths.adminStorageSwitch, {
method: 'POST',
json: { engine },
})
return String(data?.engine ?? engine)
}
/** 修改管理员密码(成功后旧 token 全部失效,需重新登录) */
export async function adminPasswordUpdate(oldPassword: string, newPassword: string): Promise<void> {
await request<void>(paths.adminPasswordUpdate, {
method: 'PATCH',
json: { old_password: oldPassword, new_password: newPassword },
})
}
export interface AuditListQuery {
page: number
size: number
action?: string
result?: string
ip?: string
/** ISO 或 YYYY-MM-DD;由调用方格式化 */
startTime?: string
endTime?: string
}
/** 审计日志列表(宽松解析兼容 data/list 字段与 snake_case/camelCase 行字段) */
export async function adminAuditList(q: AuditListQuery): Promise<AuditListResult> {
const raw = await request<Record<string, unknown>>(paths.adminAuditList, {
query: {
page: q.page,
size: q.size,
action: q.action || undefined,
result: q.result || undefined,
ip: q.ip || undefined,
start_time: q.startTime || undefined,
end_time: q.endTime || undefined,
},
})
const rows = (pick<unknown[]>(raw, ['data', 'list', 'items', 'logs']) ?? []) as Record<string, unknown>[]
const total = Number(pick<number>(raw, ['total', 'count']) ?? rows.length)
return {
page: Number(pick<number>(raw, ['page']) ?? q.page),
size: Number(pick<number>(raw, ['size']) ?? q.size),
total,
data: rows.map((r, idx) => ({
id: Number(pick<number>(r, ['id']) ?? idx + 1),
action: String(pick<string>(r, ['action']) ?? ''),
result: String(pick<string>(r, ['result']) ?? ''),
fileCode: String(pick<string>(r, ['file_code', 'fileCode', 'code']) ?? ''),
fileName: String(pick<string>(r, ['file_name', 'fileName', 'name']) ?? ''),
sizeBytes: (pick<number>(r, ['size_bytes', 'sizeBytes', 'size']) as number) ?? null,
transferredBytes: (pick<number>(r, ['transferred_bytes', 'transferredBytes', 'bytes']) as number) ?? null,
ip: String(pick<string>(r, ['ip', 'client_ip', 'clientIp']) ?? ''),
userAgent: String(pick<string>(r, ['user_agent', 'userAgent']) ?? ''),
deviceOs: String(pick<string>(r, ['device_os', 'deviceOs', 'os']) ?? ''),
deviceBrowser: String(pick<string>(r, ['device_browser', 'deviceBrowser', 'browser']) ?? ''),
deviceType: String(pick<string>(r, ['device_type', 'deviceType']) ?? ''),
actor: String(pick<string>(r, ['actor']) ?? ''),
errorMsg: String(pick<string>(r, ['error_msg', 'errorMsg', 'error']) ?? ''),
durationMs: (pick<number>(r, ['duration_ms', 'durationMs', 'duration']) as number) ?? null,
createdAt: (pick<string>(r, ['created_at', 'createdAt', 'time']) as string) ?? null,
})),
}
}