@qq33357486/oh-my-task 1.4.3 → 1.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/db/schema.sql +205 -0
  2. package/package.json +2 -2
  3. package/dist/__tests__/auth-admin.test.d.ts +0 -2
  4. package/dist/__tests__/auth-admin.test.d.ts.map +0 -1
  5. package/dist/__tests__/auth-admin.test.js +0 -440
  6. package/dist/__tests__/auth-admin.test.js.map +0 -1
  7. package/dist/__tests__/auth-login-logout.test.d.ts +0 -2
  8. package/dist/__tests__/auth-login-logout.test.d.ts.map +0 -1
  9. package/dist/__tests__/auth-login-logout.test.js +0 -400
  10. package/dist/__tests__/auth-login-logout.test.js.map +0 -1
  11. package/dist/__tests__/auth-password.test.d.ts +0 -2
  12. package/dist/__tests__/auth-password.test.d.ts.map +0 -1
  13. package/dist/__tests__/auth-password.test.js +0 -419
  14. package/dist/__tests__/auth-password.test.js.map +0 -1
  15. package/dist/__tests__/auth-register.test.d.ts +0 -2
  16. package/dist/__tests__/auth-register.test.d.ts.map +0 -1
  17. package/dist/__tests__/auth-register.test.js +0 -342
  18. package/dist/__tests__/auth-register.test.js.map +0 -1
  19. package/dist/__tests__/auth-tokens.test.d.ts +0 -2
  20. package/dist/__tests__/auth-tokens.test.d.ts.map +0 -1
  21. package/dist/__tests__/auth-tokens.test.js +0 -392
  22. package/dist/__tests__/auth-tokens.test.js.map +0 -1
  23. package/dist/__tests__/db-schema.test.d.ts +0 -2
  24. package/dist/__tests__/db-schema.test.d.ts.map +0 -1
  25. package/dist/__tests__/db-schema.test.js +0 -245
  26. package/dist/__tests__/db-schema.test.js.map +0 -1
  27. package/dist/__tests__/express-server.test.d.ts +0 -2
  28. package/dist/__tests__/express-server.test.d.ts.map +0 -1
  29. package/dist/__tests__/express-server.test.js +0 -119
  30. package/dist/__tests__/express-server.test.js.map +0 -1
  31. package/dist/__tests__/fix-hcaptcha-dev-bypass.test.d.ts +0 -2
  32. package/dist/__tests__/fix-hcaptcha-dev-bypass.test.d.ts.map +0 -1
  33. package/dist/__tests__/fix-hcaptcha-dev-bypass.test.js +0 -85
  34. package/dist/__tests__/fix-hcaptcha-dev-bypass.test.js.map +0 -1
  35. package/dist/__tests__/mcp/mcp-tools.test.d.ts +0 -2
  36. package/dist/__tests__/mcp/mcp-tools.test.d.ts.map +0 -1
  37. package/dist/__tests__/mcp/mcp-tools.test.js +0 -694
  38. package/dist/__tests__/mcp/mcp-tools.test.js.map +0 -1
  39. package/dist/__tests__/projects.test.d.ts +0 -2
  40. package/dist/__tests__/projects.test.d.ts.map +0 -1
  41. package/dist/__tests__/projects.test.js +0 -406
  42. package/dist/__tests__/projects.test.js.map +0 -1
  43. package/dist/__tests__/schedule.test.d.ts +0 -2
  44. package/dist/__tests__/schedule.test.d.ts.map +0 -1
  45. package/dist/__tests__/schedule.test.js +0 -587
  46. package/dist/__tests__/schedule.test.js.map +0 -1
  47. package/dist/__tests__/tasks-crud.test.d.ts +0 -2
  48. package/dist/__tests__/tasks-crud.test.d.ts.map +0 -1
  49. package/dist/__tests__/tasks-crud.test.js +0 -617
  50. package/dist/__tests__/tasks-crud.test.js.map +0 -1
  51. package/dist/__tests__/tasks-lifecycle.test.d.ts +0 -2
  52. package/dist/__tests__/tasks-lifecycle.test.d.ts.map +0 -1
  53. package/dist/__tests__/tasks-lifecycle.test.js +0 -712
  54. package/dist/__tests__/tasks-lifecycle.test.js.map +0 -1
  55. package/dist/__tests__/versions.test.d.ts +0 -2
  56. package/dist/__tests__/versions.test.d.ts.map +0 -1
  57. package/dist/__tests__/versions.test.js +0 -641
  58. package/dist/__tests__/versions.test.js.map +0 -1
@@ -0,0 +1,205 @@
1
+ -- oh-my-task Database Schema (v2)
2
+ -- 从零重写:移除 SOP、assignee、阶段性文档等旧功能
3
+ -- 新增 user_activity、sessions、inserted/notes 等字段
4
+
5
+ -- ============================================
6
+ -- 用户表
7
+ -- ============================================
8
+ CREATE TABLE IF NOT EXISTS users (
9
+ id TEXT PRIMARY KEY,
10
+ name TEXT NOT NULL,
11
+ email TEXT NOT NULL UNIQUE,
12
+ password_hash TEXT NOT NULL,
13
+ role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('admin', 'member')),
14
+ reset_token TEXT,
15
+ reset_token_expires DATETIME,
16
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
17
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
18
+ );
19
+
20
+ -- ============================================
21
+ -- 用户 Token 表(用于 MCP/API 认证)
22
+ -- ============================================
23
+ CREATE TABLE IF NOT EXISTS user_tokens (
24
+ id TEXT PRIMARY KEY,
25
+ user_id TEXT NOT NULL,
26
+ name TEXT NOT NULL,
27
+ token TEXT NOT NULL UNIQUE,
28
+ last_used_at DATETIME,
29
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
30
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
31
+ );
32
+
33
+ -- ============================================
34
+ -- 用户活动表(用于 DAU/留存统计)
35
+ -- ============================================
36
+ CREATE TABLE IF NOT EXISTS user_activity (
37
+ id TEXT PRIMARY KEY,
38
+ user_id TEXT NOT NULL,
39
+ action TEXT NOT NULL,
40
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
41
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
42
+ );
43
+
44
+ -- ============================================
45
+ -- 项目表
46
+ -- ============================================
47
+ CREATE TABLE IF NOT EXISTS projects (
48
+ id TEXT PRIMARY KEY,
49
+ name TEXT NOT NULL,
50
+ description TEXT,
51
+ owner_id TEXT NOT NULL,
52
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
53
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
54
+ FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE
55
+ );
56
+
57
+ -- ============================================
58
+ -- 版本表
59
+ -- ============================================
60
+ CREATE TABLE IF NOT EXISTS versions (
61
+ id TEXT PRIMARY KEY,
62
+ project_id TEXT NOT NULL,
63
+ name TEXT NOT NULL,
64
+ description TEXT,
65
+ start_date DATE,
66
+ due_date DATE,
67
+ locked_at DATETIME DEFAULT NULL,
68
+ completed_at DATETIME DEFAULT NULL,
69
+ archived_at DATETIME DEFAULT NULL,
70
+ sort_order INTEGER NOT NULL DEFAULT 0,
71
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
72
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
73
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
74
+ );
75
+
76
+ -- ============================================
77
+ -- 任务表
78
+ -- ============================================
79
+ CREATE TABLE IF NOT EXISTS tasks (
80
+ id TEXT PRIMARY KEY,
81
+ project_id TEXT NOT NULL,
82
+ version_id TEXT,
83
+ parent_id TEXT,
84
+ title TEXT NOT NULL,
85
+ description TEXT,
86
+ notes TEXT,
87
+ status TEXT NOT NULL DEFAULT 'planned' CHECK (status IN ('planned', 'in_progress', 'done')),
88
+ estimated_days INTEGER DEFAULT 1,
89
+ start_date DATE,
90
+ due_date DATE,
91
+ actual_start DATETIME,
92
+ actual_end DATETIME,
93
+ sort_order INTEGER NOT NULL DEFAULT 0,
94
+ inserted INTEGER NOT NULL DEFAULT 0 CHECK (inserted IN (0, 1)),
95
+ deleted_at DATETIME DEFAULT NULL,
96
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
97
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
98
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
99
+ FOREIGN KEY (version_id) REFERENCES versions(id) ON DELETE SET NULL,
100
+ FOREIGN KEY (parent_id) REFERENCES tasks(id) ON DELETE CASCADE
101
+ );
102
+
103
+ -- ============================================
104
+ -- 任务历史表
105
+ -- ============================================
106
+ CREATE TABLE IF NOT EXISTS task_history (
107
+ id TEXT PRIMARY KEY,
108
+ task_id TEXT NOT NULL,
109
+ action TEXT NOT NULL CHECK (action IN ('created', 'updated', 'status_changed', 'noted')),
110
+ field TEXT,
111
+ old_value TEXT,
112
+ new_value TEXT,
113
+ reason TEXT,
114
+ changed_by TEXT,
115
+ changed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
116
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
117
+ FOREIGN KEY (changed_by) REFERENCES users(id) ON DELETE SET NULL
118
+ );
119
+
120
+ -- ============================================
121
+ -- 节假日表
122
+ -- ============================================
123
+ CREATE TABLE IF NOT EXISTS holidays (
124
+ date DATE PRIMARY KEY,
125
+ year INTEGER NOT NULL,
126
+ is_workday INTEGER NOT NULL DEFAULT 0 CHECK (is_workday IN (0, 1)),
127
+ name TEXT
128
+ );
129
+
130
+ -- ============================================
131
+ -- 系统配置表
132
+ -- ============================================
133
+ CREATE TABLE IF NOT EXISTS system_config (
134
+ key TEXT PRIMARY KEY,
135
+ value TEXT NOT NULL DEFAULT '',
136
+ description TEXT,
137
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
138
+ );
139
+
140
+ -- ============================================
141
+ -- Sessions 表(express-session SQLite store)
142
+ -- better-sqlite3-session-store 需要 sid, sess, expire 列
143
+ -- ============================================
144
+ CREATE TABLE IF NOT EXISTS sessions (
145
+ sid TEXT NOT NULL PRIMARY KEY,
146
+ sess TEXT NOT NULL,
147
+ expire TEXT NOT NULL DEFAULT ''
148
+ );
149
+
150
+ -- 迁移:如果旧 sessions 表缺少 expire 列或有过时的 expired 列,进行修复
151
+ -- SQLite 不支持 DROP COLUMN,需要重建表
152
+ -- 注意:这里只在必要时执行,不影响已有 session 数据
153
+ -- better-sqlite3-session-store 会在构造时自动执行自己的 CREATE TABLE IF NOT EXISTS
154
+
155
+ -- ============================================
156
+ -- 初始系统配置
157
+ -- ============================================
158
+ INSERT OR IGNORE INTO system_config (key, value, description) VALUES
159
+ ('server_url', 'http://localhost:17173', '服务器 URL'),
160
+ ('smtp_host', '', 'SMTP 服务器地址'),
161
+ ('smtp_port', '587', 'SMTP 端口'),
162
+ ('smtp_user', '', 'SMTP 用户名'),
163
+ ('smtp_pass', '', 'SMTP 密码'),
164
+ ('smtp_from', '', '发件人邮箱'),
165
+ ('registration_enabled', '1', '是否开放注册'),
166
+ ('hcaptcha_site_key', '', 'hCaptcha Site Key'),
167
+ ('hcaptcha_secret_key', '', 'hCaptcha Secret Key');
168
+
169
+ -- ============================================
170
+ -- 索引
171
+ -- ============================================
172
+
173
+ -- users 索引
174
+ CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
175
+ CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(reset_token);
176
+
177
+ -- user_tokens 索引
178
+ CREATE INDEX IF NOT EXISTS idx_user_tokens_user_id ON user_tokens(user_id);
179
+ CREATE INDEX IF NOT EXISTS idx_user_tokens_token ON user_tokens(token);
180
+
181
+ -- user_activity 索引
182
+ CREATE INDEX IF NOT EXISTS idx_user_activity_user_id ON user_activity(user_id);
183
+ CREATE INDEX IF NOT EXISTS idx_user_activity_action ON user_activity(action);
184
+ CREATE INDEX IF NOT EXISTS idx_user_activity_created_at ON user_activity(created_at);
185
+
186
+ -- projects 索引
187
+ CREATE INDEX IF NOT EXISTS idx_projects_owner_id ON projects(owner_id);
188
+
189
+ -- versions 索引
190
+ CREATE INDEX IF NOT EXISTS idx_versions_project_id ON versions(project_id);
191
+ CREATE INDEX IF NOT EXISTS idx_versions_locked_at ON versions(locked_at);
192
+ CREATE INDEX IF NOT EXISTS idx_versions_archived_at ON versions(archived_at);
193
+
194
+ -- tasks 索引
195
+ CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id);
196
+ CREATE INDEX IF NOT EXISTS idx_tasks_parent_id ON tasks(parent_id);
197
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
198
+ CREATE INDEX IF NOT EXISTS idx_tasks_deleted_at ON tasks(deleted_at);
199
+ CREATE INDEX IF NOT EXISTS idx_tasks_version_id ON tasks(version_id);
200
+
201
+ -- task_history 索引
202
+ CREATE INDEX IF NOT EXISTS idx_task_history_task_id ON task_history(task_id);
203
+
204
+ -- holidays 索引
205
+ CREATE INDEX IF NOT EXISTS idx_holidays_year ON holidays(year);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qq33357486/oh-my-task",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "description": "文档驱动的 AI 编程协作系统 - 通过 MCP 工具管理任务",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,7 +17,7 @@
17
17
  "scripts": {
18
18
  "dev": "tsx watch src/index.ts",
19
19
  "dev:all": "tsx scripts/dev.ts",
20
- "build": "tsc",
20
+ "build": "node scripts/clean-dist.cjs && tsc && node scripts/copy-schema.cjs",
21
21
  "start": "node dist/index.js",
22
22
  "mcp": "tsx src/mcp/server.ts",
23
23
  "db:init": "tsx src/db/init.ts"
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=auth-admin.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth-admin.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/auth-admin.test.ts"],"names":[],"mappings":""}
@@ -1,440 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
2
- import request from 'supertest';
3
- import Database from 'better-sqlite3';
4
- import { mkdirSync, rmSync, existsSync, readFileSync } from 'fs';
5
- import { join } from 'path';
6
- import { tmpdir } from 'os';
7
- // 每个测试用唯一的临时目录,避免并行测试冲突
8
- let TEST_DIR;
9
- let TEST_DB_PATH;
10
- let app;
11
- // 测试用的用户凭据
12
- const ADMIN_USER = {
13
- name: 'AdminUser',
14
- email: 'admin@example.com',
15
- password: 'AdminPass123'
16
- };
17
- const MEMBER_USER = {
18
- name: 'MemberUser',
19
- email: 'member@example.com',
20
- password: 'MemberPass123'
21
- };
22
- const MEMBER_USER2 = {
23
- name: 'MemberUser2',
24
- email: 'member2@example.com',
25
- password: 'MemberPass456'
26
- };
27
- let adminCookie;
28
- let memberCookie;
29
- let member2Cookie;
30
- let adminId = 'admin-user-001';
31
- let memberId = 'member-user-001';
32
- let member2Id = 'member-user-002';
33
- beforeAll(async () => {
34
- TEST_DIR = join(tmpdir(), `omt-auth-admin-test-${Date.now()}`);
35
- TEST_DB_PATH = join(TEST_DIR, 'data', 'data.db');
36
- mkdirSync(join(TEST_DIR, 'data'), { recursive: true });
37
- process.env.DB_PATH = TEST_DB_PATH;
38
- // 初始化数据库
39
- const db = new Database(TEST_DB_PATH);
40
- db.pragma('journal_mode = WAL');
41
- db.pragma('foreign_keys = ON');
42
- const schemaSql = readFileSync(join(process.cwd(), 'src', 'db', 'schema.sql'), 'utf-8');
43
- db.exec(schemaSql);
44
- db.close();
45
- // 动态导入 app(需要在数据库初始化后)
46
- const serverModule = await import('../api/server.js');
47
- app = serverModule.default;
48
- });
49
- afterAll(() => {
50
- try {
51
- if (existsSync(TEST_DIR)) {
52
- rmSync(TEST_DIR, { recursive: true, force: true });
53
- }
54
- }
55
- catch {
56
- // Windows 可能因文件锁定无法立即删除
57
- }
58
- delete process.env.DB_PATH;
59
- });
60
- /**
61
- * 辅助函数:创建测试用户并登录
62
- */
63
- async function setupUserAndLogin(user, userId, role = 'member') {
64
- const db = new Database(TEST_DB_PATH);
65
- const bcrypt = await import('bcrypt');
66
- const hash = await bcrypt.default.hash(user.password, 12);
67
- db.prepare(`
68
- INSERT OR REPLACE INTO users (id, name, email, password_hash, role)
69
- VALUES (?, ?, ?, ?, ?)
70
- `).run(userId, user.name, user.email, hash, role);
71
- db.close();
72
- const loginRes = await request(app)
73
- .post('/api/auth/login')
74
- .send({ email: user.email, password: user.password });
75
- const setCookie = loginRes.headers['set-cookie'];
76
- if (Array.isArray(setCookie)) {
77
- return setCookie[0].split(';')[0];
78
- }
79
- return setCookie.split(';')[0];
80
- }
81
- /**
82
- * 辅助函数:在数据库中创建项目
83
- */
84
- function createProjectInDb(ownerId, projectId, name) {
85
- const db = new Database(TEST_DB_PATH);
86
- db.prepare(`
87
- INSERT INTO projects (id, name, owner_id) VALUES (?, ?, ?)
88
- `).run(projectId, name, ownerId);
89
- db.close();
90
- }
91
- /**
92
- * 辅助函数:在数据库中创建版本
93
- */
94
- function createVersionInDb(projectId, versionId, name) {
95
- const db = new Database(TEST_DB_PATH);
96
- db.prepare(`
97
- INSERT INTO versions (id, project_id, name) VALUES (?, ?, ?)
98
- `).run(versionId, projectId, name);
99
- db.close();
100
- }
101
- /**
102
- * 辅助函数:在数据库中创建任务
103
- */
104
- function createTaskInDb(projectId, taskId, title, versionId) {
105
- const db = new Database(TEST_DB_PATH);
106
- db.prepare(`
107
- INSERT INTO tasks (id, project_id, title, version_id) VALUES (?, ?, ?, ?)
108
- `).run(taskId, projectId, title, versionId || null);
109
- db.close();
110
- }
111
- /**
112
- * 辅助函数:记录用户活动
113
- */
114
- function recordActivity(userId, action, createdAt) {
115
- const db = new Database(TEST_DB_PATH);
116
- db.prepare(`
117
- INSERT INTO user_activity (id, user_id, action, created_at) VALUES (?, ?, ?, ?)
118
- `).run(`activity-${Date.now()}-${Math.random().toString(36).slice(2)}`, userId, action, createdAt || new Date().toISOString());
119
- db.close();
120
- }
121
- // ============================================
122
- // GET /api/users — 管理员查看用户列表
123
- // ============================================
124
- describe('GET /api/users', () => {
125
- beforeEach(async () => {
126
- // 清理并重建测试数据
127
- const db = new Database(TEST_DB_PATH);
128
- db.prepare('DELETE FROM user_activity');
129
- db.prepare('DELETE FROM tasks');
130
- db.prepare('DELETE FROM versions');
131
- db.prepare('DELETE FROM projects');
132
- db.prepare('DELETE FROM users');
133
- db.close();
134
- adminCookie = await setupUserAndLogin(ADMIN_USER, adminId, 'admin');
135
- memberCookie = await setupUserAndLogin(MEMBER_USER, memberId, 'member');
136
- member2Cookie = await setupUserAndLogin(MEMBER_USER2, member2Id, 'member');
137
- });
138
- it('VAL-AUTH-025: 管理员获取分页用户列表', async () => {
139
- const res = await request(app)
140
- .get('/api/users')
141
- .set('Cookie', adminCookie);
142
- expect(res.status).toBe(200);
143
- expect(res.body.success).toBe(true);
144
- expect(res.body.data.users).toBeDefined();
145
- expect(res.body.data.users.length).toBe(3); // admin + 2 members
146
- expect(res.body.data.pagination).toBeDefined();
147
- expect(res.body.data.pagination.total).toBe(3);
148
- expect(res.body.data.pagination.page).toBe(1);
149
- expect(res.body.data.pagination.total_pages).toBe(1);
150
- // 确认返回的用户包含必要字段
151
- const user = res.body.data.users[0];
152
- expect(user.id).toBeDefined();
153
- expect(user.name).toBeDefined();
154
- expect(user.email).toBeDefined();
155
- expect(user.role).toBeDefined();
156
- expect(user.created_at).toBeDefined();
157
- // 不包含密码
158
- expect(user.password_hash).toBeUndefined();
159
- });
160
- it('分页参数生效', async () => {
161
- const res = await request(app)
162
- .get('/api/users?page=1&page_size=2')
163
- .set('Cookie', adminCookie);
164
- expect(res.status).toBe(200);
165
- expect(res.body.data.users.length).toBe(2);
166
- expect(res.body.data.pagination.page_size).toBe(2);
167
- expect(res.body.data.pagination.total).toBe(3);
168
- expect(res.body.data.pagination.total_pages).toBe(2);
169
- });
170
- it('VAL-AUTH-027: 非管理员访问 GET /api/users 返回 403', async () => {
171
- const res = await request(app)
172
- .get('/api/users')
173
- .set('Cookie', memberCookie);
174
- expect(res.status).toBe(403);
175
- expect(res.body.success).toBe(false);
176
- });
177
- it('未认证访问 GET /api/users 返回 401', async () => {
178
- const res = await request(app)
179
- .get('/api/users');
180
- expect(res.status).toBe(401);
181
- expect(res.body.success).toBe(false);
182
- });
183
- });
184
- // ============================================
185
- // DELETE /api/users/:id — 管理员删除用户
186
- // ============================================
187
- describe('DELETE /api/users/:id', () => {
188
- beforeEach(async () => {
189
- const db = new Database(TEST_DB_PATH);
190
- db.prepare('DELETE FROM user_activity');
191
- db.prepare('DELETE FROM tasks');
192
- db.prepare('DELETE FROM versions');
193
- db.prepare('DELETE FROM projects');
194
- db.prepare('DELETE FROM users');
195
- db.close();
196
- adminCookie = await setupUserAndLogin(ADMIN_USER, adminId, 'admin');
197
- memberCookie = await setupUserAndLogin(MEMBER_USER, memberId, 'member');
198
- member2Cookie = await setupUserAndLogin(MEMBER_USER2, member2Id, 'member');
199
- });
200
- it('VAL-AUTH-026: 管理员删除他人返回 200', async () => {
201
- const res = await request(app)
202
- .delete(`/api/users/${memberId}`)
203
- .set('Cookie', adminCookie);
204
- expect(res.status).toBe(200);
205
- expect(res.body.success).toBe(true);
206
- expect(res.body.message).toBeDefined();
207
- // 验证用户已被删除
208
- const db = new Database(TEST_DB_PATH);
209
- const user = db.prepare('SELECT * FROM users WHERE id = ?').get(memberId);
210
- db.close();
211
- expect(user).toBeUndefined();
212
- });
213
- it('VAL-AUTH-026: 管理员删除自己返回 403', async () => {
214
- const res = await request(app)
215
- .delete(`/api/users/${adminId}`)
216
- .set('Cookie', adminCookie);
217
- expect(res.status).toBe(403);
218
- expect(res.body.success).toBe(false);
219
- });
220
- it('VAL-AUTH-027: 非管理员删除用户返回 403', async () => {
221
- const res = await request(app)
222
- .delete(`/api/users/${member2Id}`)
223
- .set('Cookie', memberCookie);
224
- expect(res.status).toBe(403);
225
- expect(res.body.success).toBe(false);
226
- });
227
- it('删除不存在的用户返回 404', async () => {
228
- const res = await request(app)
229
- .delete('/api/users/nonexistent-id')
230
- .set('Cookie', adminCookie);
231
- expect(res.status).toBe(404);
232
- expect(res.body.success).toBe(false);
233
- });
234
- it('VAL-CROSS-009: 删除用户后其项目/版本/任务被级联删除', async () => {
235
- // 为 member 用户创建项目、版本、任务
236
- createProjectInDb(memberId, 'proj-member-1', 'Member Project');
237
- createVersionInDb('proj-member-1', 'ver-member-1', 'v1');
238
- createTaskInDb('proj-member-1', 'task-member-1', 'Member Task', 'ver-member-1');
239
- // 确认数据存在
240
- const dbBefore = new Database(TEST_DB_PATH);
241
- const projBefore = dbBefore.prepare('SELECT * FROM projects WHERE owner_id = ?').all(memberId);
242
- expect(projBefore.length).toBe(1);
243
- dbBefore.close();
244
- // 管理员删除用户
245
- const res = await request(app)
246
- .delete(`/api/users/${memberId}`)
247
- .set('Cookie', adminCookie);
248
- expect(res.status).toBe(200);
249
- // 验证级联删除
250
- const dbAfter = new Database(TEST_DB_PATH);
251
- const projects = dbAfter.prepare('SELECT * FROM projects WHERE owner_id = ?').all(memberId);
252
- const versions = dbAfter.prepare('SELECT * FROM versions WHERE project_id = ?').all('proj-member-1');
253
- const tasks = dbAfter.prepare('SELECT * FROM tasks WHERE project_id = ?').all('proj-member-1');
254
- dbAfter.close();
255
- expect(projects.length).toBe(0);
256
- expect(versions.length).toBe(0);
257
- expect(tasks.length).toBe(0);
258
- });
259
- it('删除用户不影响其他用户的数据', async () => {
260
- // 为两个 member 用户创建项目
261
- createProjectInDb(memberId, 'proj-member-1', 'Member1 Project');
262
- createProjectInDb(member2Id, 'proj-member-2', 'Member2 Project');
263
- // 管理员删除 member
264
- await request(app)
265
- .delete(`/api/users/${memberId}`)
266
- .set('Cookie', adminCookie);
267
- // member2 的项目应该还在
268
- const db = new Database(TEST_DB_PATH);
269
- const proj = db.prepare('SELECT * FROM projects WHERE owner_id = ?').all(member2Id);
270
- db.close();
271
- expect(proj.length).toBe(1);
272
- expect(proj[0].name).toBe('Member2 Project');
273
- });
274
- });
275
- // ============================================
276
- // GET /api/config — 系统配置
277
- // ============================================
278
- describe('GET /api/config', () => {
279
- beforeEach(async () => {
280
- const db = new Database(TEST_DB_PATH);
281
- db.prepare('DELETE FROM users');
282
- db.close();
283
- adminCookie = await setupUserAndLogin(ADMIN_USER, adminId, 'admin');
284
- memberCookie = await setupUserAndLogin(MEMBER_USER, memberId, 'member');
285
- });
286
- it('VAL-AUTH-028: 管理员获取系统配置', async () => {
287
- const res = await request(app)
288
- .get('/api/config')
289
- .set('Cookie', adminCookie);
290
- expect(res.status).toBe(200);
291
- expect(res.body.success).toBe(true);
292
- expect(res.body.data).toBeDefined();
293
- expect(res.body.data.server_url).toBeDefined();
294
- expect(res.body.data.registration_enabled).toBeDefined();
295
- expect(res.body.data.smtp_host).toBeDefined();
296
- });
297
- it('VAL-AUTH-027: 非管理员访问 GET /api/config 返回 403', async () => {
298
- const res = await request(app)
299
- .get('/api/config')
300
- .set('Cookie', memberCookie);
301
- expect(res.status).toBe(403);
302
- expect(res.body.success).toBe(false);
303
- });
304
- });
305
- // ============================================
306
- // PUT /api/config — 更新系统配置
307
- // ============================================
308
- describe('PUT /api/config', () => {
309
- beforeEach(async () => {
310
- const db = new Database(TEST_DB_PATH);
311
- db.prepare('DELETE FROM users');
312
- db.close();
313
- adminCookie = await setupUserAndLogin(ADMIN_USER, adminId, 'admin');
314
- memberCookie = await setupUserAndLogin(MEMBER_USER, memberId, 'member');
315
- });
316
- it('VAL-AUTH-028: 管理员更新配置成功', async () => {
317
- const res = await request(app)
318
- .put('/api/config')
319
- .set('Cookie', adminCookie)
320
- .send({
321
- registration_enabled: '0',
322
- smtp_host: 'smtp.example.com'
323
- });
324
- expect(res.status).toBe(200);
325
- expect(res.body.success).toBe(true);
326
- expect(res.body.data.registration_enabled).toBe('0');
327
- expect(res.body.data.smtp_host).toBe('smtp.example.com');
328
- });
329
- it('VAL-AUTH-028: 更新后 GET 确认值已变更', async () => {
330
- // 更新配置
331
- await request(app)
332
- .put('/api/config')
333
- .set('Cookie', adminCookie)
334
- .send({
335
- registration_enabled: '0',
336
- smtp_host: 'smtp.updated.com',
337
- smtp_port: '465'
338
- });
339
- // GET 确认
340
- const getRes = await request(app)
341
- .get('/api/config')
342
- .set('Cookie', adminCookie);
343
- expect(getRes.status).toBe(200);
344
- expect(getRes.body.data.registration_enabled).toBe('0');
345
- expect(getRes.body.data.smtp_host).toBe('smtp.updated.com');
346
- expect(getRes.body.data.smtp_port).toBe('465');
347
- });
348
- it('VAL-AUTH-027: 非管理员 PUT /api/config 返回 403', async () => {
349
- const res = await request(app)
350
- .put('/api/config')
351
- .set('Cookie', memberCookie)
352
- .send({ registration_enabled: '0' });
353
- expect(res.status).toBe(403);
354
- expect(res.body.success).toBe(false);
355
- });
356
- });
357
- // ============================================
358
- // GET /api/admin/stats — 用户统计
359
- // ============================================
360
- describe('GET /api/admin/stats', () => {
361
- beforeEach(async () => {
362
- const db = new Database(TEST_DB_PATH);
363
- db.prepare('DELETE FROM user_activity');
364
- db.prepare('DELETE FROM tasks');
365
- db.prepare('DELETE FROM versions');
366
- db.prepare('DELETE FROM projects');
367
- db.prepare('DELETE FROM users');
368
- db.close();
369
- adminCookie = await setupUserAndLogin(ADMIN_USER, adminId, 'admin');
370
- memberCookie = await setupUserAndLogin(MEMBER_USER, memberId, 'member');
371
- });
372
- it('VAL-AUTH-032: 管理员获取用户统计', async () => {
373
- const res = await request(app)
374
- .get('/api/admin/stats')
375
- .set('Cookie', adminCookie);
376
- expect(res.status).toBe(200);
377
- expect(res.body.success).toBe(true);
378
- expect(res.body.data).toBeDefined();
379
- // newUsers: 日/周/月新增用户数
380
- expect(res.body.data.newUsers).toBeDefined();
381
- expect(res.body.data.newUsers.daily).toBeDefined();
382
- expect(res.body.data.newUsers.weekly).toBeDefined();
383
- expect(res.body.data.newUsers.monthly).toBeDefined();
384
- // dau: 日活用户
385
- expect(res.body.data.dau).toBeDefined();
386
- expect(Array.isArray(res.body.data.dau)).toBe(true);
387
- // retention: 留存率
388
- expect(res.body.data.retention).toBeDefined();
389
- });
390
- it('VAL-AUTH-032: 新增用户数统计准确', async () => {
391
- const res = await request(app)
392
- .get('/api/admin/stats')
393
- .set('Cookie', adminCookie);
394
- expect(res.status).toBe(200);
395
- // 今天创建了 2 个用户(admin + member),所以 daily 至少为 2
396
- const daily = res.body.data.newUsers.daily;
397
- expect(typeof daily).toBe('number');
398
- expect(daily).toBeGreaterThanOrEqual(2);
399
- });
400
- it('VAL-AUTH-032: DAU 统计返回近 7 天数据', async () => {
401
- // 为 member 用户记录一些活动
402
- recordActivity(memberId, 'login');
403
- recordActivity(adminId, 'login');
404
- const res = await request(app)
405
- .get('/api/admin/stats')
406
- .set('Cookie', adminCookie);
407
- expect(res.status).toBe(200);
408
- const dau = res.body.data.dau;
409
- expect(dau.length).toBe(7);
410
- // 每天至少有日期和数量
411
- for (const day of dau) {
412
- expect(day.date).toBeDefined();
413
- expect(day.count).toBeDefined();
414
- }
415
- });
416
- it('VAL-AUTH-032: 留存率返回数据', async () => {
417
- const res = await request(app)
418
- .get('/api/admin/stats')
419
- .set('Cookie', adminCookie);
420
- expect(res.status).toBe(200);
421
- const retention = res.body.data.retention;
422
- expect(retention).toBeDefined();
423
- // 留存率应包含 day1, day7 等字段
424
- expect(typeof retention).toBe('object');
425
- });
426
- it('VAL-AUTH-027: 非管理员访问 GET /api/admin/stats 返回 403', async () => {
427
- const res = await request(app)
428
- .get('/api/admin/stats')
429
- .set('Cookie', memberCookie);
430
- expect(res.status).toBe(403);
431
- expect(res.body.success).toBe(false);
432
- });
433
- it('未认证访问 GET /api/admin/stats 返回 401', async () => {
434
- const res = await request(app)
435
- .get('/api/admin/stats');
436
- expect(res.status).toBe(401);
437
- expect(res.body.success).toBe(false);
438
- });
439
- });
440
- //# sourceMappingURL=auth-admin.test.js.map