Files
O96u 737235acff release: v1.4.0 — 标签系统、操作日志扩展与文档截图更新
- 每用户标签与彩色打标,图库 OR/AND 筛选与批量打标
- 操作日志记录登录、删除、设置与标签操作
- API 标签/打标接口;README 与文档站截图同步
2026-09-09 14:46:52 +08:00

132 lines
3.9 KiB
TypeScript

import { requireUserAuth } from '../../utils/access'
import { createApiError } from '../../utils/api-error'
import { logException } from '../../utils/logger'
import {
listActivityLogs,
listUserIdUsernameMap,
summarizeActivityLogs
} from '../../utils/db'
import { isLogAction, isLogSource } from '../../utils/activity-log'
import { ensureStorageSchema } from '../../utils/storage-backends'
import {
DEFAULT_LIST_LIMIT,
MAX_LIST_LIMIT
} from '../../utils/constants'
export default defineEventHandler(async (event) => {
const user = await requireUserAuth(event)
const query = getQuery(event)
const limitRaw = Number(query.limit ?? DEFAULT_LIST_LIMIT)
const limit = Number.isFinite(limitRaw)
? Math.min(Math.max(1, Math.floor(limitRaw)), MAX_LIST_LIMIT)
: DEFAULT_LIST_LIMIT
const actionRaw = typeof query.action === 'string' ? query.action : ''
const action = isLogAction(actionRaw) ? actionRaw : undefined
const sourceRaw = typeof query.source === 'string' ? query.source : ''
const source = isLogSource(sourceRaw) ? sourceRaw : undefined
const folder = typeof query.folder === 'string' && query.folder.trim()
? query.folder.trim()
: undefined
const searchRaw = typeof query.q === 'string' ? query.q.trim() : ''
const search = searchRaw || undefined
const dateFromRaw = typeof query.from === 'string' ? query.from.trim() : ''
const dateToRaw = typeof query.to === 'string' ? query.to.trim() : ''
const dateFrom = dateFromRaw && /^\d{4}-\d{2}-\d{2}$/.test(dateFromRaw)
? `${dateFromRaw}T00:00:00.000Z`
: undefined
const dateTo = dateToRaw && /^\d{4}-\d{2}-\d{2}$/.test(dateToRaw)
? (() => {
const end = new Date(`${dateToRaw}T00:00:00.000Z`)
end.setUTCDate(end.getUTCDate() + 1)
return end.toISOString()
})()
: undefined
const pageRaw = Number(query.page ?? 1)
const page = Number.isFinite(pageRaw) && pageRaw > 0
? Math.floor(pageRaw)
: 1
let userId: number | undefined
if (user.role === 'admin') {
const userIdRaw = query.userId
if (userIdRaw !== undefined && userIdRaw !== '' && userIdRaw !== 'all') {
const parsed = Number(userIdRaw)
if (!Number.isFinite(parsed) || parsed <= 0) {
createApiError(event, 'INVALID_REQUEST', '无效的用户筛选', 400)
}
userId = Math.floor(parsed)
}
} else {
userId = user.id
}
try {
ensureStorageSchema()
const result = listActivityLogs({
limit,
page,
action,
source,
folder,
userId,
search,
dateFrom,
dateTo
})
const summary = summarizeActivityLogs({
source,
folder,
userId,
search,
dateFrom,
dateTo
})
const userMap = user.role === 'admin' ? listUserIdUsernameMap() : undefined
return {
items: result.items.map(row => ({
id: row.id,
action: row.action,
key: row.key,
originalName: row.original_name,
size: row.size,
contentType: row.content_type,
source: row.source,
userId: row.user_id,
username: row.username,
storage: row.backend_id
? {
id: row.backend_id,
name: row.backend_name ?? row.backend_id,
type: (row.backend_type === 'local' ? 'local' : 's3') as 'local' | 's3'
}
: null,
ipAddress: row.ip_address,
status: row.status === 'failure' ? 'failure' : 'success',
createdAt: row.created_at
})),
total: result.total,
page: result.page,
pageSize: result.pageSize,
totalPages: result.totalPages,
summary,
users: userMap
? Array.from(userMap.entries())
.map(([id, username]) => ({ id, username }))
.sort((a, b) => a.username.localeCompare(b.username))
: undefined
}
} catch (error) {
logException('activity logs list failed', error)
createApiError(event, 'INVALID_REQUEST', '读取操作记录失败', 500)
}
})