release: v1.4.0 — 标签系统、操作日志扩展与文档截图更新
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
GitHub Release / release (push) Canceled after 0s
Publish Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Canceled after 0s
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Canceled after 0s
Publish Docker image / merge (push) Canceled after 0s

- 每用户标签与彩色打标,图库 OR/AND 筛选与批量打标
- 操作日志记录登录、删除、设置与标签操作
- API 标签/打标接口;README 与文档站截图同步
This commit is contained in:
O96u committed 2026-09-09 14:46:52 +08:00
1 parent f17d79b236
commit 737235acff
110 files changed
+5881 -778

No files matched your search

+8
View File
@@ -12,6 +12,7 @@ import {
import { findUserByUsername } from '../../utils/db'
import { getAdminSecret } from '../../utils/env'
import { verifyLoginVerification, type VerificationBody } from '../../utils/login-verification'
import { logActivity } from '../../utils/activity-log'
import { clientIp, logInfo, logWarn } from '../../utils/logger'
interface LoginBody extends VerificationBody {
@@ -81,6 +82,13 @@ export default defineEventHandler(async (event) => {
await createSession(event, user.id)
logInfo('login success', { ip, username: user.username })
logActivity(event, {
action: 'login',
key: 'auth/login',
originalName: user.username,
userId: user.id,
source: 'web'
})
return {
success: true,
+4 -6
View File
@@ -1,8 +1,8 @@
import type { BatchDeleteResponse } from '~/types/image'
import { requireApiOrAdminAuth, assertImageOwnership, resolveActivitySource } from '../../utils/access'
import { requireApiOrAdminAuth, assertImageOwnership } from '../../utils/access'
import { createApiError } from '../../utils/api-error'
import { MAX_DELETE_BATCH } from '../../utils/constants'
import { insertActivityLog } from '../../utils/db'
import { logActivity } from '../../utils/activity-log'
import { getImageIndexRow, deleteImageIndex } from '../../utils/image-index'
import { toCanonicalImageKey, validateImageKey } from '../../utils/image-key'
import { deleteImage, headImage, resolveStorageImageKey } from '../../utils/storage'
@@ -67,13 +67,12 @@ export default defineEventHandler(async (event) => {
if (storageKey !== key) deleteImageIndex(storageKey)
deleted.push(key)
insertActivityLog({
logActivity(event, {
action: 'delete',
key,
originalName: indexed.original_name,
size: indexed.size,
contentType: indexed.content_type,
source: resolveActivitySource(event),
userId: indexed.user_id ?? null,
backendId: indexed.backend_id ?? null
})
@@ -102,13 +101,12 @@ export default defineEventHandler(async (event) => {
await deleteImage(storageKey, key)
deleted.push(key)
insertActivityLog({
logActivity(event, {
action: 'delete',
key,
originalName: existing.originalName,
size: existing.size,
contentType: existing.contentType,
source: resolveActivitySource(event),
userId: existing.userId ?? null,
backendId: existing.backendId ?? null
})
+95
View File
@@ -0,0 +1,95 @@
import { requireApiOrAdminAuth } from '../../utils/access'
import { logActivity } from '../../utils/activity-log'
import { getCurrentUser } from '../../utils/auth'
import { createApiError } from '../../utils/api-error'
import { MAX_DELETE_BATCH } from '../../utils/constants'
import { getImageIndexRow } from '../../utils/image-index'
import { toCanonicalImageKey, validateImageKey } from '../../utils/image-key'
import {
addTagsToImage,
removeTagFromImage
} from '../../utils/tags'
import { handleTagApiError } from '../../utils/tag-api'
interface BatchTagsBody {
keys?: string[]
tagIds?: number[]
action?: 'add' | 'remove'
}
function validateKeys(keys: string[]): void {
for (const key of keys) {
if (
!validateImageKey(key)
&& !toCanonicalImageKey(key)
&& !getImageIndexRow(key)
) {
throw new Error(`INVALID_KEY:${key}`)
}
}
}
export default defineEventHandler(async (event) => {
await requireApiOrAdminAuth(event)
const user = await getCurrentUser(event)
const body = await readBody<BatchTagsBody>(event)
const keys = Array.isArray(body?.keys)
? body.keys.filter(key => typeof key === 'string' && key.trim())
: []
const tagIds = Array.isArray(body?.tagIds)
? body.tagIds.filter(id => Number.isInteger(id) && id > 0)
: []
const action = body?.action === 'remove' ? 'remove' : 'add'
if (!keys.length) {
createApiError(event, 'INVALID_REQUEST', '请提供图片列表', 400)
}
if (!tagIds.length) {
createApiError(event, 'INVALID_REQUEST', '请提供标签列表', 400)
}
if (keys.length > MAX_DELETE_BATCH) {
createApiError(
event,
'INVALID_REQUEST',
`每次最多处理 ${MAX_DELETE_BATCH} 张图片`,
400
)
}
try {
validateKeys(keys)
} catch (error) {
if (error instanceof Error && error.message.startsWith('INVALID_KEY:')) {
createApiError(event, 'INVALID_IMAGE_KEY', `无效的图片路径: ${error.message.slice(12)}`, 400)
}
throw error
}
let updated = 0
try {
for (const key of keys) {
if (action === 'add') {
addTagsToImage(key, tagIds, user!.id, user!.role === 'admin')
} else {
for (const tagId of tagIds) {
removeTagFromImage(key, tagId, user!.id, user!.role === 'admin')
}
}
updated++
}
} catch (error) {
handleTagApiError(event, error)
}
logActivity(event, {
action: 'tags',
key: 'images/batch-tags',
originalName: action === 'add'
? `批量打标(${keys.length} 张)`
: `批量移除标签(${keys.length} 张)`,
userId: user!.id
})
return { success: true, updated }
})
+6 -2
View File
@@ -1,5 +1,5 @@
import { getImageUserFilter, requireApiOrAdminAuth } from '../../utils/access'
import { readBackendIdQuery } from '../../utils/image-query'
import { readBackendIdQuery, readTagFilterQuery } from '../../utils/image-query'
import { createApiError } from '../../utils/api-error'
import { countImages } from '../../utils/storage'
@@ -12,8 +12,12 @@ export default defineEventHandler(async (event) => {
if (backendId === null) {
createApiError(event, 'INVALID_REQUEST', '无效的存储后端', 400)
}
const tagFilter = readTagFilterQuery(query)
if (tagFilter === null) {
createApiError(event, 'INVALID_REQUEST', '无效的标签筛选参数', 400)
}
const total = await countImages(userFilter, backendId)
const total = await countImages(userFilter, backendId, tagFilter)
return { total }
})
+4 -6
View File
@@ -1,7 +1,7 @@
import type { DeleteResponse } from '~/types/image'
import { requireApiOrAdminAuth, assertImageOwnership, resolveActivitySource } from '../../utils/access'
import { requireApiOrAdminAuth, assertImageOwnership } from '../../utils/access'
import { createApiError } from '../../utils/api-error'
import { insertActivityLog } from '../../utils/db'
import { logActivity } from '../../utils/activity-log'
import { getImageIndexRow, deleteImageIndex } from '../../utils/image-index'
import { toCanonicalImageKey, validateImageKey } from '../../utils/image-key'
import { deleteImage, headImage, resolveStorageImageKey } from '../../utils/storage'
@@ -40,13 +40,12 @@ export default defineEventHandler(async (event) => {
deleteImageIndex(key)
if (storageKey !== key) deleteImageIndex(storageKey)
insertActivityLog({
logActivity(event, {
action: 'delete',
key,
originalName: indexed.original_name,
size: indexed.size,
contentType: indexed.content_type,
source: resolveActivitySource(event),
userId: indexed.user_id ?? null,
backendId: indexed.backend_id ?? null
})
@@ -69,13 +68,12 @@ export default defineEventHandler(async (event) => {
createApiError(event, 'DELETE_FAILED', '删除失败', 500)
}
insertActivityLog({
logActivity(event, {
action: 'delete',
key,
originalName: existing.originalName,
size: existing.size,
contentType: existing.contentType,
source: resolveActivitySource(event),
userId: existing.userId ?? null,
backendId: existing.backendId ?? null
})
+12 -5
View File
@@ -7,9 +7,9 @@ import {
} from '../../utils/constants'
import { createApiError } from '../../utils/api-error'
import { getUploadSourcesForKeys, listUserIdUsernameMap } from '../../utils/db'
import { mapStoredImageToItem } from '../../utils/image-response'
import { mapStoredImageToItem, attachTagsToImageItems } from '../../utils/image-response'
import { listStorageBackendNameMap } from '../../utils/storage-backends'
import { readBackendIdQuery, readContentTypeQuery, readUploadSourceQuery } from '../../utils/image-query'
import { readBackendIdQuery, readContentTypeQuery, readTagFilterQuery, readUploadSourceQuery } from '../../utils/image-query'
import { listImages } from '../../utils/storage'
export default defineEventHandler(async (event) => {
@@ -39,6 +39,10 @@ export default defineEventHandler(async (event) => {
if (uploadSource === null) {
createApiError(event, 'INVALID_REQUEST', '无效的上传来源', 400)
}
const tagFilter = readTagFilterQuery(query)
if (tagFilter === null) {
createApiError(event, 'INVALID_REQUEST', '无效的标签筛选参数', 400)
}
const userFilter = await getImageUserFilter(event)
const listing = await listImages({
@@ -47,15 +51,18 @@ export default defineEventHandler(async (event) => {
userFilter,
backendId,
contentType,
uploadSource
uploadSource,
tagFilter
})
const user = await getCurrentUser(event)
const ownerMap = user?.role === 'admin' ? listUserIdUsernameMap() : undefined
const backendMap = listStorageBackendNameMap()
const sourceMap = getUploadSourcesForKeys(listing.items.map(item => item.key))
const items = listing.items.map(stored =>
mapStoredImageToItem(event, stored, { ownerMap, backendMap, sourceMap })
const items = attachTagsToImageItems(
listing.items.map(stored =>
mapStoredImageToItem(event, stored, { ownerMap, backendMap, sourceMap })
)
)
items.sort((a, b) => b.uploadedAt.localeCompare(a.uploadedAt))
+12 -5
View File
@@ -7,9 +7,9 @@ import {
} from '../../utils/constants'
import { createApiError } from '../../utils/api-error'
import { getUploadSourcesForKeys, listUserIdUsernameMap } from '../../utils/db'
import { mapStoredImageToItem } from '../../utils/image-response'
import { mapStoredImageToItem, attachTagsToImageItems } from '../../utils/image-response'
import { listStorageBackendNameMap } from '../../utils/storage-backends'
import { readBackendIdQuery, readContentTypeQuery, readUploadSourceQuery } from '../../utils/image-query'
import { readBackendIdQuery, readContentTypeQuery, readTagFilterQuery, readUploadSourceQuery } from '../../utils/image-query'
import { searchImages } from '../../utils/storage'
export default defineEventHandler(async (event) => {
@@ -48,6 +48,10 @@ export default defineEventHandler(async (event) => {
if (uploadSource === null) {
createApiError(event, 'INVALID_REQUEST', '无效的上传来源', 400)
}
const tagFilter = readTagFilterQuery(query)
if (tagFilter === null) {
createApiError(event, 'INVALID_REQUEST', '无效的标签筛选参数', 400)
}
const userFilter = await getImageUserFilter(event)
const result = await searchImages({
@@ -57,15 +61,18 @@ export default defineEventHandler(async (event) => {
userFilter,
backendId,
contentType,
uploadSource
uploadSource,
tagFilter
})
const user = await getCurrentUser(event)
const ownerMap = user?.role === 'admin' ? listUserIdUsernameMap() : undefined
const backendMap = listStorageBackendNameMap()
const sourceMap = getUploadSourcesForKeys(result.items.map(item => item.key))
const items = result.items.map(stored =>
mapStoredImageToItem(event, stored, { ownerMap, backendMap, sourceMap })
const items = attachTagsToImageItems(
result.items.map(stored =>
mapStoredImageToItem(event, stored, { ownerMap, backendMap, sourceMap })
)
)
items.sort((a, b) => b.uploadedAt.localeCompare(a.uploadedAt))
+60
View File
@@ -0,0 +1,60 @@
import { requireApiOrAdminAuth } from '../../utils/access'
import { logActivity } from '../../utils/activity-log'
import { getCurrentUser } from '../../utils/auth'
import { createApiError } from '../../utils/api-error'
import { getImageIndexRow } from '../../utils/image-index'
import { toCanonicalImageKey, validateImageKey } from '../../utils/image-key'
import { removeTagFromImage } from '../../utils/tags'
import { handleTagApiError } from '../../utils/tag-api'
interface ImageTagDeleteBody {
key?: string
tagId?: number
}
export default defineEventHandler(async (event) => {
await requireApiOrAdminAuth(event)
const user = await getCurrentUser(event)
const query = getQuery(event)
const body = await readBody<ImageTagDeleteBody>(event).catch(() => ({} as ImageTagDeleteBody))
const key = (typeof query.key === 'string' ? query.key : undefined) ?? body?.key?.trim()
const tagId = Number(
typeof query.tagId === 'string' ? query.tagId : body?.tagId
)
if (
!key
|| (
!validateImageKey(key)
&& !toCanonicalImageKey(key)
&& !getImageIndexRow(key)
)
) {
createApiError(event, 'INVALID_IMAGE_KEY', '无效的图片路径', 400)
}
if (!Number.isInteger(tagId) || tagId <= 0) {
createApiError(event, 'INVALID_REQUEST', '无效的标签 ID', 400)
}
try {
const tags = removeTagFromImage(
key,
tagId,
user!.id,
user!.role === 'admin'
)
const indexed = getImageIndexRow(key)
logActivity(event, {
action: 'tags',
key,
originalName: indexed?.original_name ?? key,
userId: user!.id
})
return { tags }
} catch (error) {
handleTagApiError(event, error)
}
})
+54
View File
@@ -0,0 +1,54 @@
import { requireApiOrAdminAuth } from '../../utils/access'
import { logActivity } from '../../utils/activity-log'
import { getCurrentUser } from '../../utils/auth'
import { createApiError } from '../../utils/api-error'
import { getImageIndexRow } from '../../utils/image-index'
import { toCanonicalImageKey, validateImageKey } from '../../utils/image-key'
import { setImageTags } from '../../utils/tags'
import { handleTagApiError } from '../../utils/tag-api'
interface ImageTagsBody {
key?: string
tagIds?: number[]
}
export default defineEventHandler(async (event) => {
await requireApiOrAdminAuth(event)
const user = await getCurrentUser(event)
const body = await readBody<ImageTagsBody>(event)
const key = body?.key?.trim()
if (
!key
|| (
!validateImageKey(key)
&& !toCanonicalImageKey(key)
&& !getImageIndexRow(key)
)
) {
createApiError(event, 'INVALID_IMAGE_KEY', '无效的图片路径', 400)
}
const tagIds = Array.isArray(body?.tagIds)
? body.tagIds.filter(id => Number.isInteger(id) && id > 0)
: []
try {
const tags = setImageTags(
key,
tagIds,
user!.id,
user!.role === 'admin'
)
const indexed = getImageIndexRow(key)
logActivity(event, {
action: 'tags',
key,
originalName: indexed?.original_name ?? key,
userId: user!.id
})
return { tags }
} catch (error) {
handleTagApiError(event, error)
}
})
+57
View File
@@ -0,0 +1,57 @@
import { requireApiOrAdminAuth } from '../../utils/access'
import { logActivity } from '../../utils/activity-log'
import { getCurrentUser } from '../../utils/auth'
import { createApiError } from '../../utils/api-error'
import { getImageIndexRow } from '../../utils/image-index'
import { toCanonicalImageKey, validateImageKey } from '../../utils/image-key'
import { addTagsToImage } from '../../utils/tags'
import { handleTagApiError } from '../../utils/tag-api'
interface ImageTagsBody {
key?: string
tagIds?: number[]
}
export default defineEventHandler(async (event) => {
await requireApiOrAdminAuth(event)
const user = await getCurrentUser(event)
const body = await readBody<ImageTagsBody>(event)
const key = body?.key?.trim()
if (
!key
|| (
!validateImageKey(key)
&& !toCanonicalImageKey(key)
&& !getImageIndexRow(key)
)
) {
createApiError(event, 'INVALID_IMAGE_KEY', '无效的图片路径', 400)
}
const tagIds = Array.isArray(body?.tagIds)
? body.tagIds.filter(id => Number.isInteger(id) && id > 0)
: []
if (!tagIds.length) {
createApiError(event, 'INVALID_REQUEST', '请提供要添加的标签', 400)
}
try {
const tags = addTagsToImage(
key,
tagIds,
user!.id,
user!.role === 'admin'
)
const indexed = getImageIndexRow(key)
logActivity(event, {
action: 'tags',
key,
originalName: indexed?.original_name ?? key,
userId: user!.id
})
return { tags }
} catch (error) {
handleTagApiError(event, error)
}
})
+22 -1
View File
@@ -12,6 +12,7 @@ import {
} from '../../utils/process-image-upload'
import { createApiError } from '../../utils/api-error'
import { checkUploadRateLimit } from '../../utils/rate-limit'
import { parseTagIdsParam } from '../../utils/tags'
export default defineEventHandler(async (event) => {
await requireUploadAuth(event)
@@ -30,6 +31,25 @@ export default defineEventHandler(async (event) => {
const source = verifyApiUploadToken(event) ? 'api' : 'web'
const uploadUserId = await getUploadUserId(event)
const tagIdsField = formData.find(part => part.name === 'tagIds')
let uploadTagIds: number[] = []
if (tagIdsField?.data?.length) {
const raw = new TextDecoder().decode(tagIdsField.data).trim()
try {
const parsed = JSON.parse(raw) as unknown
uploadTagIds = parseTagIdsParam(parsed)
} catch {
uploadTagIds = parseTagIdsParam(raw)
}
} else {
const repeatFields = formData.filter(part => part.name === 'tagIds' && part.data?.length)
if (repeatFields.length) {
uploadTagIds = parseTagIdsParam(
repeatFields.map(part => new TextDecoder().decode(part.data!).trim())
)
}
}
// 兼容 image / file / files 字段名(图床脚本 / 通用客户端 / 网页多图)
const fileParts = formData.filter(
part => (part.name === 'image' || part.name === 'file' || part.name === 'files')
@@ -83,7 +103,8 @@ export default defineEventHandler(async (event) => {
bytes,
filename: part.filename,
source,
userId: uploadUserId
userId: uploadUserId,
tagIds: uploadTagIds
})
if ('error' in result) {
+38 -11
View File
@@ -4,10 +4,9 @@ import { logException } from '../../utils/logger'
import {
listActivityLogs,
listUserIdUsernameMap,
summarizeActivityLogs,
type LogAction,
type LogSource
summarizeActivityLogs
} from '../../utils/db'
import { isLogAction, isLogSource } from '../../utils/activity-log'
import { ensureStorageSchema } from '../../utils/storage-backends'
import {
DEFAULT_LIST_LIMIT,
@@ -25,14 +24,10 @@ export default defineEventHandler(async (event) => {
: DEFAULT_LIST_LIMIT
const actionRaw = typeof query.action === 'string' ? query.action : ''
const action = (actionRaw === 'upload' || actionRaw === 'delete')
? actionRaw as LogAction
: undefined
const action = isLogAction(actionRaw) ? actionRaw : undefined
const sourceRaw = typeof query.source === 'string' ? query.source : ''
const source = (sourceRaw === 'web' || sourceRaw === 'api')
? sourceRaw as LogSource
: undefined
const source = isLogSource(sourceRaw) ? sourceRaw : undefined
const folder = typeof query.folder === 'string' && query.folder.trim()
? query.folder.trim()
@@ -41,6 +36,19 @@ export default defineEventHandler(async (event) => {
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)
@@ -62,8 +70,25 @@ export default defineEventHandler(async (event) => {
try {
ensureStorageSchema()
const result = listActivityLogs({ limit, page, action, source, folder, userId, search })
const summary = summarizeActivityLogs({ source, folder, userId, search })
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 {
@@ -84,6 +109,8 @@ export default defineEventHandler(async (event) => {
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,
+10 -1
View File
@@ -1,4 +1,5 @@
import { requireAdminAuth } from '../../utils/access'
import { logActivity } from '../../utils/activity-log'
import { createApiError } from '../../utils/api-error'
import {
getSetting,
@@ -52,7 +53,7 @@ interface SettingsPatchBody {
}
export default defineEventHandler(async (event) => {
await requireAdminAuth(event)
const user = await requireAdminAuth(event)
const body = await readBody<SettingsPatchBody>(event).catch(
(): SettingsPatchBody => ({})
@@ -248,5 +249,13 @@ export default defineEventHandler(async (event) => {
setSetting(SETTINGS_CAP_SECRET, nextCapSecret)
}
logActivity(event, {
action: 'settings',
key: 'settings/system',
originalName: '系统设置',
userId: user.id,
source: 'admin'
})
return getSettingsPayload(event)
})
+9 -3
View File
@@ -2,6 +2,7 @@ import { getImageUserFilter, requireUserAuth } from '../utils/access'
import { createApiError } from '../utils/api-error'
import { logException } from '../utils/logger'
import { countUsers, getActivityStats } from '../utils/db'
import { readTagFilterQuery } from '../utils/image-query'
import {
countImages,
getFolderStorageStats,
@@ -39,13 +40,18 @@ async function resolveStorageUsage(usedBytes: number): Promise<StorageUsageStat>
export default defineEventHandler(async (event) => {
const user = await requireUserAuth(event)
const userFilter = await getImageUserFilter(event)
const query = getQuery(event)
const tagFilter = readTagFilterQuery(query)
if (tagFilter === null) {
createApiError(event, 'INVALID_REQUEST', '无效的标签筛选参数', 400)
}
try {
if (userFilter === 'admin') {
const activity = getActivityStats()
const [storedCount, byFolder, storageUsage] = await Promise.all([
countImages(),
getFolderStorageStats(),
countImages('admin', undefined, tagFilter),
getFolderStorageStats('admin', tagFilter),
resolveStorageUsage(activity.uploadBytesTotal)
])
@@ -58,7 +64,7 @@ export default defineEventHandler(async (event) => {
}
}
const scoped = await getUserScopedStorageStats(user.id)
const scoped = await getUserScopedStorageStats(user.id, tagFilter)
const storageUsage = await resolveStorageUsage(scoped.uploadBytesTotal)
return {
+32
View File
@@ -0,0 +1,32 @@
import { requireUserAuth } from '../../utils/access'
import { logActivity } from '../../utils/activity-log'
import { createApiError } from '../../utils/api-error'
import { deleteTag, getTagForUser } from '../../utils/tags'
export default defineEventHandler(async (event) => {
const user = await requireUserAuth(event)
const id = Number(getRouterParam(event, 'id'))
if (!Number.isInteger(id) || id <= 0) {
createApiError(event, 'INVALID_REQUEST', '无效的标签 ID', 400)
}
const existing = getTagForUser(user.id, id)
if (!existing) {
createApiError(event, 'INVALID_REQUEST', '标签不存在', 404)
}
const deleted = deleteTag(user.id, id)
if (!deleted) {
createApiError(event, 'INVALID_REQUEST', '标签不存在', 404)
}
logActivity(event, {
action: 'tags',
key: `tags/${id}`,
originalName: existing.name,
userId: user.id
})
return { success: true }
})
+40
View File
@@ -0,0 +1,40 @@
import { requireUserAuth } from '../../utils/access'
import { logActivity } from '../../utils/activity-log'
import { createApiError } from '../../utils/api-error'
import { updateTag } from '../../utils/tags'
import { handleTagApiError } from '../../utils/tag-api'
interface PatchTagBody {
name?: string
color?: string | null
}
export default defineEventHandler(async (event) => {
const user = await requireUserAuth(event)
const id = Number(getRouterParam(event, 'id'))
if (!Number.isInteger(id) || id <= 0) {
createApiError(event, 'INVALID_REQUEST', '无效的标签 ID', 400)
}
const body = await readBody<PatchTagBody>(event)
if (body.name === undefined && body.color === undefined) {
createApiError(event, 'INVALID_REQUEST', '请提供要更新的字段', 400)
}
try {
const tag = updateTag(user.id, id, {
name: body.name,
color: body.color
})
logActivity(event, {
action: 'edit',
key: `tags/${tag.id}`,
originalName: tag.name,
userId: user.id
})
return { tag }
} catch (error) {
handleTagApiError(event, error)
}
})
+7
View File
@@ -0,0 +1,7 @@
import { requireUserAuth } from '../../utils/access'
import { listTagsForUser } from '../../utils/tags'
export default defineEventHandler(async (event) => {
const user = await requireUserAuth(event)
return { items: listTagsForUser(user.id) }
})
+32
View File
@@ -0,0 +1,32 @@
import { requireUserAuth } from '../../utils/access'
import { logActivity } from '../../utils/activity-log'
import { createApiError } from '../../utils/api-error'
import { createTag } from '../../utils/tags'
import { handleTagApiError } from '../../utils/tag-api'
interface CreateTagBody {
name?: string
color?: string | null
}
export default defineEventHandler(async (event) => {
const user = await requireUserAuth(event)
const body = await readBody<CreateTagBody>(event)
if (!body?.name || typeof body.name !== 'string') {
createApiError(event, 'INVALID_REQUEST', '请提供标签名称', 400)
}
try {
const tag = createTag(user.id, body.name, body.color)
logActivity(event, {
action: 'tags',
key: `tags/${tag.id}`,
originalName: tag.name,
userId: user.id
})
return { tag }
} catch (error) {
handleTagApiError(event, error)
}
})
+37
View File
@@ -0,0 +1,37 @@
import { requireUserAuth } from '../../utils/access'
import { logActivity } from '../../utils/activity-log'
import { createApiError } from '../../utils/api-error'
import { mergeTags } from '../../utils/tags'
import { handleTagApiError } from '../../utils/tag-api'
interface MergeTagsBody {
sourceIds?: number[]
targetId?: number
}
export default defineEventHandler(async (event) => {
const user = await requireUserAuth(event)
const body = await readBody<MergeTagsBody>(event)
const sourceIds = Array.isArray(body?.sourceIds)
? body.sourceIds.filter(id => Number.isInteger(id) && id > 0)
: []
const targetId = Number(body?.targetId)
if (!sourceIds.length || !Number.isInteger(targetId) || targetId <= 0) {
createApiError(event, 'INVALID_REQUEST', '请提供有效的源标签与目标标签', 400)
}
try {
const tag = mergeTags(user.id, sourceIds, targetId)
logActivity(event, {
action: 'tags',
key: `tags/${tag.id}`,
originalName: `合并标签 → ${tag.name}`,
userId: user.id
})
return { tag }
} catch (error) {
handleTagApiError(event, error)
}
})
+11
View File
@@ -1,4 +1,5 @@
import { requireUserAuth } from '../../utils/access'
import { logActivity } from '../../utils/activity-log'
import { createApiError } from '../../utils/api-error'
import { getUserAutoDeletePolicy, setUserAutoDeletePolicy } from '../../utils/db'
import { MAX_AUTO_DELETE_DAYS, parseAutoDeleteDays } from '../../utils/env'
@@ -28,6 +29,16 @@ export default defineEventHandler(async (event) => {
}
const policy = getUserAutoDeletePolicy(user.id)
if (body.autoDeleteDays !== undefined) {
logActivity(event, {
action: 'settings',
key: 'user/settings',
originalName: '上传偏好',
userId: user.id
})
}
return {
autoDeleteDays: policy.days
}