@ian2018cs/agenthub 0.1.1 → 0.1.2
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.
- package/dist/assets/index-BITEl9tD.js +154 -0
- package/dist/assets/index-BWbEF217.css +32 -0
- package/dist/assets/{vendor-icons-CJV4dnDL.js → vendor-icons-q7OlK-Uk.js} +76 -61
- package/dist/index.html +3 -3
- package/package.json +2 -1
- package/server/builtin-skills/deploy-frontend/SKILL.md +29 -1
- package/server/builtin-skills/deploy-frontend/scripts/cleanup.py +26 -1
- package/server/builtin-skills/deploy-frontend/scripts/deploy.py +30 -5
- package/server/database/db.js +246 -2
- package/server/database/init.sql +35 -3
- package/server/middleware/auth.js +5 -4
- package/server/routes/admin.js +113 -2
- package/server/routes/auth.js +113 -38
- package/server/services/email.js +90 -0
- package/dist/assets/index-B4ru3EJb.css +0 -32
- package/dist/assets/index-DDFuyrpY.js +0 -154
|
@@ -14,6 +14,31 @@ import time
|
|
|
14
14
|
# 默认 nginx 基础目录(可在这里修改)
|
|
15
15
|
DEFAULT_NGINX_BASE_DIR = "/home/xubuntu001/AI/nginx"
|
|
16
16
|
|
|
17
|
+
def run_docker_command(cmd, **kwargs):
|
|
18
|
+
"""
|
|
19
|
+
运行 docker 命令,自动处理权限问题
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
cmd: 命令列表
|
|
23
|
+
**kwargs: 传递给 subprocess.run 的其他参数
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
subprocess.CompletedProcess 对象
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
# 首先尝试直接运行
|
|
30
|
+
return subprocess.run(cmd, **kwargs)
|
|
31
|
+
except (subprocess.CalledProcessError, PermissionError) as e:
|
|
32
|
+
# 如果失败,尝试使用 sg docker -c 运行
|
|
33
|
+
if 'check' in kwargs:
|
|
34
|
+
del kwargs['check'] # sg 会处理 check
|
|
35
|
+
|
|
36
|
+
# 将命令转换为 sg docker -c 格式
|
|
37
|
+
cmd_str = ' '.join(str(arg) for arg in cmd)
|
|
38
|
+
sg_cmd = ['sg', 'docker', '-c', cmd_str]
|
|
39
|
+
|
|
40
|
+
return subprocess.run(sg_cmd, **kwargs)
|
|
41
|
+
|
|
17
42
|
def find_available_port(start_port=8080, max_attempts=100):
|
|
18
43
|
"""查找可用端口"""
|
|
19
44
|
for port in range(start_port, start_port + max_attempts):
|
|
@@ -58,7 +83,7 @@ def create_nginx_conf(project_id, port, nginx_base_dir):
|
|
|
58
83
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
|
|
59
84
|
|
|
60
85
|
# 缓存静态资源
|
|
61
|
-
location ~*
|
|
86
|
+
location ~* \\.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {{
|
|
62
87
|
expires 1y;
|
|
63
88
|
add_header Cache-Control "public, immutable";
|
|
64
89
|
}}
|
|
@@ -78,7 +103,7 @@ def reload_nginx(nginx_base_dir):
|
|
|
78
103
|
raise RuntimeError(f"docker-compose.yml 不存在: {compose_file}")
|
|
79
104
|
|
|
80
105
|
# 检查容器是否运行
|
|
81
|
-
result =
|
|
106
|
+
result = run_docker_command(
|
|
82
107
|
["docker", "ps", "--filter", "name=nginx-web", "--format", "{{.Names}}"],
|
|
83
108
|
capture_output=True,
|
|
84
109
|
text=True,
|
|
@@ -88,15 +113,15 @@ def reload_nginx(nginx_base_dir):
|
|
|
88
113
|
if "nginx-web" not in result.stdout:
|
|
89
114
|
# 容器未运行,启动它
|
|
90
115
|
print("启动 nginx 容器...")
|
|
91
|
-
|
|
92
|
-
["docker
|
|
116
|
+
run_docker_command(
|
|
117
|
+
["docker", "compose", "up", "-d"],
|
|
93
118
|
cwd=nginx_base_dir,
|
|
94
119
|
check=True
|
|
95
120
|
)
|
|
96
121
|
else:
|
|
97
122
|
# 重新加载配置
|
|
98
123
|
print("重新加载 nginx 配置...")
|
|
99
|
-
|
|
124
|
+
run_docker_command(
|
|
100
125
|
["docker", "exec", "nginx-web", "nginx", "-s", "reload"],
|
|
101
126
|
check=True
|
|
102
127
|
)
|
package/server/database/db.js
CHANGED
|
@@ -63,6 +63,23 @@ const runMigrations = () => {
|
|
|
63
63
|
const tableInfo = db.prepare("PRAGMA table_info(users)").all();
|
|
64
64
|
const columnNames = tableInfo.map(col => col.name);
|
|
65
65
|
|
|
66
|
+
// Create verification_codes table for email login
|
|
67
|
+
db.exec(`
|
|
68
|
+
CREATE TABLE IF NOT EXISTS verification_codes (
|
|
69
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
70
|
+
email TEXT NOT NULL,
|
|
71
|
+
code TEXT NOT NULL,
|
|
72
|
+
type TEXT DEFAULT 'login' CHECK(type IN ('login', 'register')),
|
|
73
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
74
|
+
expires_at DATETIME NOT NULL,
|
|
75
|
+
attempts INTEGER DEFAULT 0,
|
|
76
|
+
used BOOLEAN DEFAULT 0,
|
|
77
|
+
ip_address TEXT
|
|
78
|
+
)
|
|
79
|
+
`);
|
|
80
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_verification_codes_email ON verification_codes(email)');
|
|
81
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_verification_codes_expires ON verification_codes(expires_at)');
|
|
82
|
+
|
|
66
83
|
// Create usage_records table for tracking token usage
|
|
67
84
|
db.exec(`
|
|
68
85
|
CREATE TABLE IF NOT EXISTS usage_records (
|
|
@@ -138,6 +155,25 @@ const runMigrations = () => {
|
|
|
138
155
|
// Create index for status (safe to run even if already exists)
|
|
139
156
|
db.exec('CREATE INDEX IF NOT EXISTS idx_users_status ON users(status)');
|
|
140
157
|
|
|
158
|
+
// Add email column if not exists (for email verification login)
|
|
159
|
+
if (!columnNames.includes('email')) {
|
|
160
|
+
console.log('Running migration: Adding email column');
|
|
161
|
+
db.exec('ALTER TABLE users ADD COLUMN email TEXT');
|
|
162
|
+
}
|
|
163
|
+
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email)');
|
|
164
|
+
|
|
165
|
+
// Create email domain whitelist table
|
|
166
|
+
db.exec(`
|
|
167
|
+
CREATE TABLE IF NOT EXISTS email_domain_whitelist (
|
|
168
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
169
|
+
domain TEXT UNIQUE NOT NULL,
|
|
170
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
171
|
+
created_by INTEGER,
|
|
172
|
+
FOREIGN KEY (created_by) REFERENCES users(id)
|
|
173
|
+
)
|
|
174
|
+
`);
|
|
175
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_email_domain_whitelist_domain ON email_domain_whitelist(domain)');
|
|
176
|
+
|
|
141
177
|
console.log('Database migrations completed successfully');
|
|
142
178
|
} catch (error) {
|
|
143
179
|
console.error('Error running migrations:', error.message);
|
|
@@ -282,7 +318,7 @@ const userDb = {
|
|
|
282
318
|
getAllUsers: () => {
|
|
283
319
|
try {
|
|
284
320
|
return db.prepare(
|
|
285
|
-
'SELECT id, username, uuid, role, status, created_at, last_login FROM users ORDER BY created_at DESC'
|
|
321
|
+
'SELECT id, username, email, uuid, role, status, created_at, last_login FROM users ORDER BY created_at DESC'
|
|
286
322
|
).all();
|
|
287
323
|
} catch (err) {
|
|
288
324
|
throw err;
|
|
@@ -314,6 +350,142 @@ const userDb = {
|
|
|
314
350
|
} catch (err) {
|
|
315
351
|
throw err;
|
|
316
352
|
}
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
// Get user by email
|
|
356
|
+
getUserByEmail: (email) => {
|
|
357
|
+
try {
|
|
358
|
+
return db.prepare('SELECT * FROM users WHERE email = ? AND is_active = 1').get(email);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
throw err;
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
// Create user with email (for email verification login)
|
|
365
|
+
createUserWithEmail: (email, uuid, role) => {
|
|
366
|
+
try {
|
|
367
|
+
const stmt = db.prepare(
|
|
368
|
+
'INSERT INTO users (email, uuid, role) VALUES (?, ?, ?)'
|
|
369
|
+
);
|
|
370
|
+
const result = stmt.run(email, uuid, role);
|
|
371
|
+
return { id: result.lastInsertRowid, email, uuid, role };
|
|
372
|
+
} catch (err) {
|
|
373
|
+
throw err;
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
// Check if email exists
|
|
378
|
+
emailExists: (email) => {
|
|
379
|
+
try {
|
|
380
|
+
const row = db.prepare('SELECT COUNT(*) as count FROM users WHERE email = ?').get(email);
|
|
381
|
+
return row.count > 0;
|
|
382
|
+
} catch (err) {
|
|
383
|
+
throw err;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// Verification codes database operations
|
|
389
|
+
const verificationDb = {
|
|
390
|
+
// Generate and store verification code
|
|
391
|
+
createCode: (email, type = 'login', ipAddress = null) => {
|
|
392
|
+
try {
|
|
393
|
+
const code = Math.floor(100000 + Math.random() * 900000).toString(); // 6-digit code
|
|
394
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString(); // 5 minutes
|
|
395
|
+
|
|
396
|
+
const stmt = db.prepare(`
|
|
397
|
+
INSERT INTO verification_codes (email, code, type, expires_at, ip_address)
|
|
398
|
+
VALUES (?, ?, ?, ?, ?)
|
|
399
|
+
`);
|
|
400
|
+
const result = stmt.run(email, code, type, expiresAt, ipAddress);
|
|
401
|
+
return { id: result.lastInsertRowid, code };
|
|
402
|
+
} catch (err) {
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
// Verify code and check if valid
|
|
408
|
+
verifyCode: (email, code) => {
|
|
409
|
+
try {
|
|
410
|
+
const now = new Date().toISOString();
|
|
411
|
+
|
|
412
|
+
// Find valid, unused code
|
|
413
|
+
const record = db.prepare(`
|
|
414
|
+
SELECT * FROM verification_codes
|
|
415
|
+
WHERE email = ? AND code = ? AND used = 0 AND expires_at > ?
|
|
416
|
+
ORDER BY created_at DESC LIMIT 1
|
|
417
|
+
`).get(email, code, now);
|
|
418
|
+
|
|
419
|
+
if (!record) {
|
|
420
|
+
return { valid: false, error: 'invalid_code' };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (record.attempts >= 5) {
|
|
424
|
+
return { valid: false, error: 'max_attempts' };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Mark as used
|
|
428
|
+
db.prepare('UPDATE verification_codes SET used = 1 WHERE id = ?').run(record.id);
|
|
429
|
+
|
|
430
|
+
return { valid: true, type: record.type };
|
|
431
|
+
} catch (err) {
|
|
432
|
+
throw err;
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
// Increment attempt count
|
|
437
|
+
incrementAttempts: (email, code) => {
|
|
438
|
+
try {
|
|
439
|
+
db.prepare(`
|
|
440
|
+
UPDATE verification_codes SET attempts = attempts + 1
|
|
441
|
+
WHERE email = ? AND code = ? AND used = 0
|
|
442
|
+
`).run(email, code);
|
|
443
|
+
} catch (err) {
|
|
444
|
+
throw err;
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
// Check rate limit for sending codes
|
|
449
|
+
canSendCode: (email) => {
|
|
450
|
+
try {
|
|
451
|
+
const oneMinuteAgo = new Date(Date.now() - 60 * 1000).toISOString();
|
|
452
|
+
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
|
|
453
|
+
|
|
454
|
+
// Check per-email rate limit (1 per minute)
|
|
455
|
+
const recentByEmail = db.prepare(`
|
|
456
|
+
SELECT COUNT(*) as count FROM verification_codes
|
|
457
|
+
WHERE email = ? AND created_at > ?
|
|
458
|
+
`).get(email, oneMinuteAgo);
|
|
459
|
+
|
|
460
|
+
if (recentByEmail.count >= 1) {
|
|
461
|
+
return { allowed: false, error: 'rate_limit_email', waitSeconds: 60 };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Check per-email hourly limit (10 per hour)
|
|
465
|
+
const hourlyByEmail = db.prepare(`
|
|
466
|
+
SELECT COUNT(*) as count FROM verification_codes
|
|
467
|
+
WHERE email = ? AND created_at > ?
|
|
468
|
+
`).get(email, oneHourAgo);
|
|
469
|
+
|
|
470
|
+
if (hourlyByEmail.count >= 10) {
|
|
471
|
+
return { allowed: false, error: 'rate_limit_hourly' };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return { allowed: true };
|
|
475
|
+
} catch (err) {
|
|
476
|
+
throw err;
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
// Cleanup expired codes
|
|
481
|
+
cleanupExpired: () => {
|
|
482
|
+
try {
|
|
483
|
+
const now = new Date().toISOString();
|
|
484
|
+
const result = db.prepare('DELETE FROM verification_codes WHERE expires_at < ?').run(now);
|
|
485
|
+
return result.changes;
|
|
486
|
+
} catch (err) {
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
317
489
|
}
|
|
318
490
|
};
|
|
319
491
|
|
|
@@ -516,9 +688,81 @@ const usageDb = {
|
|
|
516
688
|
}
|
|
517
689
|
};
|
|
518
690
|
|
|
691
|
+
// Email domain whitelist database operations
|
|
692
|
+
const domainWhitelistDb = {
|
|
693
|
+
// Get all whitelisted domains
|
|
694
|
+
getAllDomains: () => {
|
|
695
|
+
try {
|
|
696
|
+
return db.prepare('SELECT * FROM email_domain_whitelist ORDER BY domain ASC').all();
|
|
697
|
+
} catch (err) {
|
|
698
|
+
throw err;
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
// Add a domain to whitelist
|
|
703
|
+
addDomain: (domain, createdBy = null) => {
|
|
704
|
+
try {
|
|
705
|
+
const normalizedDomain = domain.toLowerCase().trim();
|
|
706
|
+
const stmt = db.prepare('INSERT INTO email_domain_whitelist (domain, created_by) VALUES (?, ?)');
|
|
707
|
+
const result = stmt.run(normalizedDomain, createdBy);
|
|
708
|
+
return { id: result.lastInsertRowid, domain: normalizedDomain };
|
|
709
|
+
} catch (err) {
|
|
710
|
+
if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
|
711
|
+
throw new Error('域名已存在');
|
|
712
|
+
}
|
|
713
|
+
throw err;
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
|
|
717
|
+
// Remove a domain from whitelist
|
|
718
|
+
removeDomain: (id) => {
|
|
719
|
+
try {
|
|
720
|
+
const result = db.prepare('DELETE FROM email_domain_whitelist WHERE id = ?').run(id);
|
|
721
|
+
return result.changes > 0;
|
|
722
|
+
} catch (err) {
|
|
723
|
+
throw err;
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
|
|
727
|
+
// Check if email domain is allowed
|
|
728
|
+
isEmailAllowed: (email) => {
|
|
729
|
+
try {
|
|
730
|
+
// First check if whitelist is empty (allow all if no restrictions)
|
|
731
|
+
const count = db.prepare('SELECT COUNT(*) as count FROM email_domain_whitelist').get();
|
|
732
|
+
if (count.count === 0) {
|
|
733
|
+
return true; // No whitelist configured, allow all
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Extract domain from email
|
|
737
|
+
const domain = email.toLowerCase().split('@')[1];
|
|
738
|
+
if (!domain) {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Check if domain is in whitelist
|
|
743
|
+
const row = db.prepare('SELECT id FROM email_domain_whitelist WHERE domain = ?').get(domain);
|
|
744
|
+
return !!row;
|
|
745
|
+
} catch (err) {
|
|
746
|
+
throw err;
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
|
|
750
|
+
// Get whitelist count
|
|
751
|
+
getCount: () => {
|
|
752
|
+
try {
|
|
753
|
+
const row = db.prepare('SELECT COUNT(*) as count FROM email_domain_whitelist').get();
|
|
754
|
+
return row.count;
|
|
755
|
+
} catch (err) {
|
|
756
|
+
throw err;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
|
|
519
761
|
export {
|
|
520
762
|
db,
|
|
521
763
|
initializeDatabase,
|
|
522
764
|
userDb,
|
|
523
|
-
usageDb
|
|
765
|
+
usageDb,
|
|
766
|
+
verificationDb,
|
|
767
|
+
domainWhitelistDb
|
|
524
768
|
};
|
package/server/database/init.sql
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
PRAGMA foreign_keys = ON;
|
|
3
3
|
|
|
4
4
|
-- Users table (multi-user system)
|
|
5
|
+
-- Supports two login methods:
|
|
6
|
+
-- 1. Email verification code (email field, no password_hash)
|
|
7
|
+
-- 2. Username/password (username + password_hash, created by admin)
|
|
5
8
|
CREATE TABLE IF NOT EXISTS users (
|
|
6
9
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
7
|
-
username TEXT UNIQUE
|
|
8
|
-
password_hash TEXT
|
|
10
|
+
username TEXT UNIQUE,
|
|
11
|
+
password_hash TEXT,
|
|
12
|
+
email TEXT UNIQUE,
|
|
9
13
|
uuid TEXT,
|
|
10
14
|
role TEXT DEFAULT 'user' CHECK(role IN ('admin', 'user')),
|
|
11
15
|
status TEXT DEFAULT 'active' CHECK(status IN ('active', 'disabled')),
|
|
@@ -20,4 +24,32 @@ CREATE TABLE IF NOT EXISTS users (
|
|
|
20
24
|
-- Indexes for performance (base indexes only)
|
|
21
25
|
-- Note: Indexes for uuid, role, status are created in migrations to support upgrades
|
|
22
26
|
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
|
23
|
-
CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
29
|
+
|
|
30
|
+
-- Verification codes table for email login
|
|
31
|
+
CREATE TABLE IF NOT EXISTS verification_codes (
|
|
32
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
33
|
+
email TEXT NOT NULL,
|
|
34
|
+
code TEXT NOT NULL,
|
|
35
|
+
type TEXT DEFAULT 'login' CHECK(type IN ('login', 'register')),
|
|
36
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
37
|
+
expires_at DATETIME NOT NULL,
|
|
38
|
+
attempts INTEGER DEFAULT 0,
|
|
39
|
+
used BOOLEAN DEFAULT 0,
|
|
40
|
+
ip_address TEXT
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_verification_codes_email ON verification_codes(email);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_verification_codes_expires ON verification_codes(expires_at);
|
|
45
|
+
|
|
46
|
+
-- Email domain whitelist for registration
|
|
47
|
+
CREATE TABLE IF NOT EXISTS email_domain_whitelist (
|
|
48
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
49
|
+
domain TEXT UNIQUE NOT NULL,
|
|
50
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
51
|
+
created_by INTEGER,
|
|
52
|
+
FOREIGN KEY (created_by) REFERENCES users(id)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_email_domain_whitelist_domain ON email_domain_whitelist(domain);
|
|
@@ -65,17 +65,18 @@ const authenticateToken = async (req, res, next) => {
|
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
// Generate JWT token
|
|
68
|
+
// Generate JWT token with 30-day expiration
|
|
69
69
|
const generateToken = (user) => {
|
|
70
70
|
return jwt.sign(
|
|
71
71
|
{
|
|
72
72
|
userId: user.id,
|
|
73
|
-
username: user.username,
|
|
73
|
+
username: user.username || user.email,
|
|
74
|
+
email: user.email,
|
|
74
75
|
uuid: user.uuid,
|
|
75
76
|
role: user.role
|
|
76
77
|
},
|
|
77
|
-
JWT_SECRET
|
|
78
|
-
|
|
78
|
+
JWT_SECRET,
|
|
79
|
+
{ expiresIn: '30d' } // 30-day expiration
|
|
79
80
|
);
|
|
80
81
|
};
|
|
81
82
|
|
package/server/routes/admin.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
import
|
|
2
|
+
import bcrypt from 'bcrypt';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
import { userDb, domainWhitelistDb } from '../database/db.js';
|
|
3
5
|
import { authenticateToken } from '../middleware/auth.js';
|
|
4
|
-
import { deleteUserDirectories } from '../services/user-directories.js';
|
|
6
|
+
import { deleteUserDirectories, initUserDirectories } from '../services/user-directories.js';
|
|
5
7
|
|
|
6
8
|
const router = express.Router();
|
|
7
9
|
|
|
@@ -28,6 +30,58 @@ router.get('/users', (req, res) => {
|
|
|
28
30
|
}
|
|
29
31
|
});
|
|
30
32
|
|
|
33
|
+
// Create a new user with username and password (admin only)
|
|
34
|
+
router.post('/users', async (req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
const { username, password } = req.body;
|
|
37
|
+
|
|
38
|
+
if (!username || !password) {
|
|
39
|
+
return res.status(400).json({ error: '用户名和密码不能为空' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (username.length < 3 || password.length < 6) {
|
|
43
|
+
return res.status(400).json({
|
|
44
|
+
error: '用户名至少3个字符,密码至少6个字符'
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check if username already exists
|
|
49
|
+
const existingUser = userDb.getUserByUsername(username);
|
|
50
|
+
if (existingUser) {
|
|
51
|
+
return res.status(409).json({ error: '用户名已存在' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Hash password
|
|
55
|
+
const saltRounds = 12;
|
|
56
|
+
const passwordHash = await bcrypt.hash(password, saltRounds);
|
|
57
|
+
|
|
58
|
+
// Create user
|
|
59
|
+
const uuid = uuidv4();
|
|
60
|
+
const user = userDb.createUserFull(username, passwordHash, uuid, 'user');
|
|
61
|
+
|
|
62
|
+
// Initialize user directories
|
|
63
|
+
await initUserDirectories(uuid);
|
|
64
|
+
|
|
65
|
+
res.json({
|
|
66
|
+
success: true,
|
|
67
|
+
user: {
|
|
68
|
+
id: user.id,
|
|
69
|
+
username: user.username,
|
|
70
|
+
uuid: user.uuid,
|
|
71
|
+
role: user.role
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Error creating user:', error);
|
|
77
|
+
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
|
78
|
+
res.status(409).json({ error: '用户名已存在' });
|
|
79
|
+
} else {
|
|
80
|
+
res.status(500).json({ error: '创建用户失败' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
31
85
|
// Update user status
|
|
32
86
|
router.patch('/users/:id', (req, res) => {
|
|
33
87
|
try {
|
|
@@ -86,4 +140,61 @@ router.delete('/users/:id', async (req, res) => {
|
|
|
86
140
|
}
|
|
87
141
|
});
|
|
88
142
|
|
|
143
|
+
// ==================== Email Domain Whitelist ====================
|
|
144
|
+
|
|
145
|
+
// Get all whitelisted domains
|
|
146
|
+
router.get('/email-domains', (req, res) => {
|
|
147
|
+
try {
|
|
148
|
+
const domains = domainWhitelistDb.getAllDomains();
|
|
149
|
+
res.json({ domains });
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('Error fetching email domains:', error);
|
|
152
|
+
res.status(500).json({ error: '获取域名列表失败' });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Add a domain to whitelist
|
|
157
|
+
router.post('/email-domains', (req, res) => {
|
|
158
|
+
try {
|
|
159
|
+
const { domain } = req.body;
|
|
160
|
+
|
|
161
|
+
if (!domain) {
|
|
162
|
+
return res.status(400).json({ error: '域名不能为空' });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Validate domain format
|
|
166
|
+
const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/;
|
|
167
|
+
if (!domainRegex.test(domain)) {
|
|
168
|
+
return res.status(400).json({ error: '域名格式无效' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const result = domainWhitelistDb.addDomain(domain, req.user.id);
|
|
172
|
+
res.json({ success: true, domain: result });
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error('Error adding email domain:', error);
|
|
175
|
+
if (error.message === '域名已存在') {
|
|
176
|
+
res.status(409).json({ error: error.message });
|
|
177
|
+
} else {
|
|
178
|
+
res.status(500).json({ error: '添加域名失败' });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Remove a domain from whitelist
|
|
184
|
+
router.delete('/email-domains/:id', (req, res) => {
|
|
185
|
+
try {
|
|
186
|
+
const { id } = req.params;
|
|
187
|
+
const success = domainWhitelistDb.removeDomain(parseInt(id));
|
|
188
|
+
|
|
189
|
+
if (success) {
|
|
190
|
+
res.json({ success: true, message: '域名已删除' });
|
|
191
|
+
} else {
|
|
192
|
+
res.status(404).json({ error: '域名不存在' });
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('Error removing email domain:', error);
|
|
196
|
+
res.status(500).json({ error: '删除域名失败' });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
89
200
|
export default router;
|