@ian2018cs/agenthub 0.1.0 → 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/.gitkeep +0 -0
- package/server/builtin-skills/deploy-frontend/SKILL.md +220 -0
- package/server/builtin-skills/deploy-frontend/scripts/cleanup.py +158 -0
- package/server/builtin-skills/deploy-frontend/scripts/deploy.py +216 -0
- package/server/cli.js +7 -6
- package/server/database/db.js +262 -17
- 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/routes/skills.js +8 -0
- package/server/services/builtin-skills.js +147 -0
- package/server/services/email.js +90 -0
- package/server/services/user-directories.js +4 -0
- package/dist/assets/index-B4ru3EJb.css +0 -32
- package/dist/assets/index-DDFuyrpY.js +0 -154
package/server/database/db.js
CHANGED
|
@@ -21,25 +21,25 @@ const c = {
|
|
|
21
21
|
dim: (text) => `${colors.dim}${text}${colors.reset}`,
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
// Use DATABASE_PATH environment variable if set, otherwise use
|
|
25
|
-
//
|
|
24
|
+
// Use DATABASE_PATH environment variable if set, otherwise use DATA_DIR/auth.db
|
|
25
|
+
// DATA_DIR defaults to ./data relative to project root
|
|
26
|
+
const PROJECT_ROOT = path.join(__dirname, '../..');
|
|
27
|
+
const DATA_DIR = process.env.DATA_DIR || path.join(PROJECT_ROOT, 'data');
|
|
26
28
|
const DB_PATH = process.env.DATABASE_PATH
|
|
27
|
-
? path.resolve(
|
|
28
|
-
: path.join(
|
|
29
|
+
? path.resolve(PROJECT_ROOT, process.env.DATABASE_PATH)
|
|
30
|
+
: path.join(DATA_DIR, 'auth.db');
|
|
29
31
|
const INIT_SQL_PATH = path.join(__dirname, 'init.sql');
|
|
30
32
|
|
|
31
|
-
// Ensure database directory exists
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
console.log(`Created database directory: ${dbDir}`);
|
|
38
|
-
}
|
|
39
|
-
} catch (error) {
|
|
40
|
-
console.error(`Failed to create database directory ${dbDir}:`, error.message);
|
|
41
|
-
throw error;
|
|
33
|
+
// Ensure database directory exists
|
|
34
|
+
const dbDir = path.dirname(DB_PATH);
|
|
35
|
+
try {
|
|
36
|
+
if (!fs.existsSync(dbDir)) {
|
|
37
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
38
|
+
console.log(`Created database directory: ${dbDir}`);
|
|
42
39
|
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(`Failed to create database directory ${dbDir}:`, error.message);
|
|
42
|
+
throw error;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Create database connection
|
|
@@ -50,6 +50,7 @@ const appInstallPath = path.join(__dirname, '../..');
|
|
|
50
50
|
console.log('');
|
|
51
51
|
console.log(c.dim('═'.repeat(60)));
|
|
52
52
|
console.log(`${c.info('[INFO]')} App Installation: ${c.bright(appInstallPath)}`);
|
|
53
|
+
console.log(`${c.info('[INFO]')} Data Directory: ${c.dim(path.relative(appInstallPath, DATA_DIR))}`);
|
|
53
54
|
console.log(`${c.info('[INFO]')} Database: ${c.dim(path.relative(appInstallPath, DB_PATH))}`);
|
|
54
55
|
if (process.env.DATABASE_PATH) {
|
|
55
56
|
console.log(` ${c.dim('(Using custom DATABASE_PATH from environment)')}`);
|
|
@@ -62,6 +63,23 @@ const runMigrations = () => {
|
|
|
62
63
|
const tableInfo = db.prepare("PRAGMA table_info(users)").all();
|
|
63
64
|
const columnNames = tableInfo.map(col => col.name);
|
|
64
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
|
+
|
|
65
83
|
// Create usage_records table for tracking token usage
|
|
66
84
|
db.exec(`
|
|
67
85
|
CREATE TABLE IF NOT EXISTS usage_records (
|
|
@@ -137,6 +155,25 @@ const runMigrations = () => {
|
|
|
137
155
|
// Create index for status (safe to run even if already exists)
|
|
138
156
|
db.exec('CREATE INDEX IF NOT EXISTS idx_users_status ON users(status)');
|
|
139
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
|
+
|
|
140
177
|
console.log('Database migrations completed successfully');
|
|
141
178
|
} catch (error) {
|
|
142
179
|
console.error('Error running migrations:', error.message);
|
|
@@ -281,7 +318,7 @@ const userDb = {
|
|
|
281
318
|
getAllUsers: () => {
|
|
282
319
|
try {
|
|
283
320
|
return db.prepare(
|
|
284
|
-
'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'
|
|
285
322
|
).all();
|
|
286
323
|
} catch (err) {
|
|
287
324
|
throw err;
|
|
@@ -313,6 +350,142 @@ const userDb = {
|
|
|
313
350
|
} catch (err) {
|
|
314
351
|
throw err;
|
|
315
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
|
+
}
|
|
316
489
|
}
|
|
317
490
|
};
|
|
318
491
|
|
|
@@ -515,9 +688,81 @@ const usageDb = {
|
|
|
515
688
|
}
|
|
516
689
|
};
|
|
517
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
|
+
|
|
518
761
|
export {
|
|
519
762
|
db,
|
|
520
763
|
initializeDatabase,
|
|
521
764
|
userDb,
|
|
522
|
-
usageDb
|
|
765
|
+
usageDb,
|
|
766
|
+
verificationDb,
|
|
767
|
+
domainWhitelistDb
|
|
523
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;
|