feat: 支持注销用户并展示普通用户使用情况
- 新增管理员注销用户接口,要求输入用户名二次确认 - 注销时清理用户当前图片、视频、文件、标签、会话和自身日志 - 保留备份任务、管理员审计数据和全局配置 - 用户列表展示图片、视频、文件占用,管理员行隐藏使用情况和操作按钮 - 补充注销流程与用量统计测试
This commit is contained in:
1 parent
dd0005b7ba
commit
ccfdac2e1b
8 files changed
+394
-24
No files matched your search
@@ -5,7 +5,8 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getDb,
|
||||
insertActivityLog,
|
||||
insertUser
|
||||
insertUser,
|
||||
listAdminUsersWithUsage
|
||||
} from './db'
|
||||
import {
|
||||
listImageIndexRowsForUser,
|
||||
@@ -16,7 +17,7 @@ import { ensureDefaultBackends } from './storage-backends'
|
||||
import { getActiveBackend } from './storage/resolver'
|
||||
import { putImage } from './storage'
|
||||
import { createTag } from './tags'
|
||||
import { clearUserStorage } from './user-storage'
|
||||
import { clearUserStorage, deleteUserCurrentData } from './user-storage'
|
||||
import {
|
||||
createBackupJob,
|
||||
getBackupJob,
|
||||
@@ -142,4 +143,81 @@ describe('clearUserStorage', () => {
|
||||
await expect(fs.access(archiveA)).rejects.toThrow()
|
||||
await expect(fs.access(archiveB)).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('deletes current user data without deleting backups or admin audit data', async () => {
|
||||
ensureDefaultBackends()
|
||||
const now = new Date().toISOString()
|
||||
const admin = insertUser({
|
||||
username: 'delete-admin',
|
||||
passwordHash: 'admin-hash',
|
||||
role: 'admin',
|
||||
createdAt: now
|
||||
})
|
||||
const user = insertUser({
|
||||
username: 'delete-user',
|
||||
passwordHash: 'user-hash',
|
||||
role: 'user',
|
||||
createdAt: now
|
||||
})
|
||||
const key = 'images/2026/10/delete-user.webp'
|
||||
const target = await getActiveBackend()
|
||||
|
||||
await putImage(key, new Uint8Array([1]), {
|
||||
originalName: 'delete-user.webp',
|
||||
uploadedAt: now,
|
||||
contentType: 'image/webp',
|
||||
size: 1,
|
||||
userId: user,
|
||||
mediaType: 'image'
|
||||
})
|
||||
const tag = createTag(user, 'delete tag')
|
||||
getDb().prepare('INSERT INTO image_tags (image_key, tag_id) VALUES (?, ?)')
|
||||
.run(key, tag.id)
|
||||
getDb().prepare(`
|
||||
INSERT INTO sessions (id, user_id, token_hash, expires_at, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run('delete-session', user, 'hash', now, now)
|
||||
insertActivityLog({
|
||||
action: 'upload',
|
||||
key,
|
||||
originalName: 'delete-user.webp',
|
||||
size: 1,
|
||||
contentType: 'image/webp',
|
||||
source: 'web',
|
||||
userId: user
|
||||
})
|
||||
insertActivityLog({
|
||||
action: 'settings',
|
||||
key: `users/${user}/clear-password`,
|
||||
originalName: 'delete-user',
|
||||
size: 0,
|
||||
contentType: '',
|
||||
source: 'admin',
|
||||
userId: admin
|
||||
})
|
||||
const job = createBackupJob({ type: 'full_export', userId: user })
|
||||
markJobDone(job.id, {})
|
||||
|
||||
const listed = listAdminUsersWithUsage().find(item => item.id === user)
|
||||
expect(listed?.usage.imageBytes).toBe(1)
|
||||
|
||||
const result = await deleteUserCurrentData(user)
|
||||
|
||||
expect(result.failed).toEqual([])
|
||||
expect(result.deletedMedia).toBe(1)
|
||||
expect(result.deletedTags).toBe(1)
|
||||
expect(await target.head(key)).toBeNull()
|
||||
expect(getDb().prepare('SELECT 1 FROM users WHERE id = ?').get(user)).toBeUndefined()
|
||||
expect(getDb().prepare('SELECT 1 FROM sessions WHERE user_id = ?').get(user)).toBeUndefined()
|
||||
expect(getDb().prepare('SELECT 1 FROM images WHERE user_id = ?').get(user)).toBeUndefined()
|
||||
expect(getDb().prepare('SELECT 1 FROM tags WHERE user_id = ?').get(user)).toBeUndefined()
|
||||
expect(getDb().prepare('SELECT 1 FROM activity_logs WHERE user_id = ?').get(user)).toBeUndefined()
|
||||
expect(getDb().prepare(`
|
||||
SELECT original_name FROM activity_logs
|
||||
WHERE user_id = ? AND key = ?
|
||||
`).get(admin, `users/${user}/clear-password`)).toEqual({ original_name: '已注销用户' })
|
||||
expect(getDb().prepare('SELECT 1 FROM backup_jobs WHERE id = ?').get(job.id)).toBeDefined()
|
||||
|
||||
await expect(deleteUserCurrentData(admin)).rejects.toThrow('ADMIN_USER_DELETE_FORBIDDEN')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user