@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
package/server/routes/auth.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import bcrypt from 'bcrypt';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
-
import { userDb,
|
|
4
|
+
import { userDb, verificationDb, domainWhitelistDb } from '../database/db.js';
|
|
5
5
|
import { generateToken, authenticateToken } from '../middleware/auth.js';
|
|
6
6
|
import { initUserDirectories } from '../services/user-directories.js';
|
|
7
|
+
import { sendVerificationCode, isSmtpConfigured } from '../services/email.js';
|
|
7
8
|
|
|
8
9
|
const router = express.Router();
|
|
9
10
|
|
|
@@ -11,9 +12,10 @@ const router = express.Router();
|
|
|
11
12
|
router.get('/status', async (req, res) => {
|
|
12
13
|
try {
|
|
13
14
|
const hasUsers = await userDb.hasUsers();
|
|
14
|
-
res.json({
|
|
15
|
+
res.json({
|
|
15
16
|
needsSetup: !hasUsers,
|
|
16
|
-
isAuthenticated: false // Will be overridden by frontend if token exists
|
|
17
|
+
isAuthenticated: false, // Will be overridden by frontend if token exists
|
|
18
|
+
smtpConfigured: isSmtpConfigured()
|
|
17
19
|
});
|
|
18
20
|
} catch (error) {
|
|
19
21
|
console.error('Auth status error:', error);
|
|
@@ -21,37 +23,106 @@ router.get('/status', async (req, res) => {
|
|
|
21
23
|
}
|
|
22
24
|
});
|
|
23
25
|
|
|
24
|
-
//
|
|
25
|
-
router.post('/
|
|
26
|
+
// Send verification code to email
|
|
27
|
+
router.post('/send-code', async (req, res) => {
|
|
26
28
|
try {
|
|
27
|
-
const {
|
|
29
|
+
const { email } = req.body;
|
|
30
|
+
const ipAddress = req.ip || req.socket?.remoteAddress;
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
// Validate email format
|
|
33
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
34
|
+
if (!email || !emailRegex.test(email)) {
|
|
35
|
+
return res.status(400).json({ error: '请输入有效的邮箱地址' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if SMTP is configured
|
|
39
|
+
if (!isSmtpConfigured()) {
|
|
40
|
+
return res.status(500).json({ error: 'SMTP 未配置,请联系管理员' });
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
// Check rate limit
|
|
44
|
+
const rateCheck = verificationDb.canSendCode(email);
|
|
45
|
+
if (!rateCheck.allowed) {
|
|
46
|
+
if (rateCheck.error === 'rate_limit_email') {
|
|
47
|
+
return res.status(429).json({
|
|
48
|
+
error: '发送过于频繁,请稍后再试',
|
|
49
|
+
waitSeconds: rateCheck.waitSeconds
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return res.status(429).json({ error: '今日发送次数已达上限' });
|
|
37
53
|
}
|
|
38
54
|
|
|
39
|
-
//
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const uuid = uuidv4();
|
|
55
|
+
// Determine if this is login or registration
|
|
56
|
+
const existingUser = userDb.getUserByEmail(email);
|
|
57
|
+
const type = existingUser ? 'login' : 'register';
|
|
43
58
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
// For new registrations, check domain whitelist
|
|
60
|
+
if (type === 'register') {
|
|
61
|
+
if (!domainWhitelistDb.isEmailAllowed(email)) {
|
|
62
|
+
return res.status(403).json({ error: '该邮箱域名不在允许注册的列表中' });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Generate and store code
|
|
67
|
+
const { code } = verificationDb.createCode(email, type, ipAddress);
|
|
68
|
+
|
|
69
|
+
// Send email
|
|
70
|
+
await sendVerificationCode(email, code);
|
|
47
71
|
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
res.json({
|
|
73
|
+
success: true,
|
|
74
|
+
type, // 'login' or 'register' - frontend can show appropriate message
|
|
75
|
+
message: '验证码已发送'
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('Send code error:', error);
|
|
80
|
+
res.status(500).json({ error: '发送验证码失败,请稍后再试' });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Verify code and login/register
|
|
85
|
+
router.post('/verify-code', async (req, res) => {
|
|
86
|
+
try {
|
|
87
|
+
const { email, code } = req.body;
|
|
88
|
+
|
|
89
|
+
if (!email || !code) {
|
|
90
|
+
return res.status(400).json({ error: '邮箱和验证码不能为空' });
|
|
91
|
+
}
|
|
50
92
|
|
|
51
|
-
//
|
|
52
|
-
|
|
93
|
+
// Verify the code
|
|
94
|
+
const verification = verificationDb.verifyCode(email, code);
|
|
53
95
|
|
|
54
|
-
|
|
96
|
+
if (!verification.valid) {
|
|
97
|
+
// Increment attempts for failed verification
|
|
98
|
+
verificationDb.incrementAttempts(email, code);
|
|
99
|
+
|
|
100
|
+
if (verification.error === 'max_attempts') {
|
|
101
|
+
return res.status(429).json({ error: '验证码尝试次数过多,请重新获取' });
|
|
102
|
+
}
|
|
103
|
+
return res.status(401).json({ error: '验证码无效或已过期' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let user = userDb.getUserByEmail(email);
|
|
107
|
+
|
|
108
|
+
// If user doesn't exist, create new user (registration)
|
|
109
|
+
if (!user) {
|
|
110
|
+
const userCount = userDb.getUserCount();
|
|
111
|
+
const role = userCount === 0 ? 'admin' : 'user';
|
|
112
|
+
const uuid = uuidv4();
|
|
113
|
+
|
|
114
|
+
user = userDb.createUserWithEmail(email, uuid, role);
|
|
115
|
+
|
|
116
|
+
// Initialize user directories
|
|
117
|
+
await initUserDirectories(uuid);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check if user is disabled
|
|
121
|
+
if (user.status === 'disabled') {
|
|
122
|
+
return res.status(403).json({ error: '账户已被禁用' });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Generate token with 30-day expiration
|
|
55
126
|
const token = generateToken(user);
|
|
56
127
|
|
|
57
128
|
// Update last login
|
|
@@ -61,7 +132,8 @@ router.post('/register', async (req, res) => {
|
|
|
61
132
|
success: true,
|
|
62
133
|
user: {
|
|
63
134
|
id: user.id,
|
|
64
|
-
|
|
135
|
+
email: user.email,
|
|
136
|
+
username: user.username || user.email,
|
|
65
137
|
uuid: user.uuid,
|
|
66
138
|
role: user.role
|
|
67
139
|
},
|
|
@@ -69,37 +141,38 @@ router.post('/register', async (req, res) => {
|
|
|
69
141
|
});
|
|
70
142
|
|
|
71
143
|
} catch (error) {
|
|
72
|
-
console.error('
|
|
73
|
-
|
|
74
|
-
res.status(409).json({ error: 'Username already exists' });
|
|
75
|
-
} else {
|
|
76
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
77
|
-
}
|
|
144
|
+
console.error('Verify code error:', error);
|
|
145
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
78
146
|
}
|
|
79
147
|
});
|
|
80
148
|
|
|
81
|
-
// User login
|
|
149
|
+
// User login with username/password (for admin-created accounts)
|
|
82
150
|
router.post('/login', async (req, res) => {
|
|
83
151
|
try {
|
|
84
152
|
const { username, password } = req.body;
|
|
85
153
|
|
|
86
154
|
if (!username || !password) {
|
|
87
|
-
return res.status(400).json({ error: '
|
|
155
|
+
return res.status(400).json({ error: '用户名和密码不能为空' });
|
|
88
156
|
}
|
|
89
157
|
|
|
90
158
|
const user = userDb.getUserByUsername(username);
|
|
91
159
|
if (!user) {
|
|
92
|
-
return res.status(401).json({ error: '
|
|
160
|
+
return res.status(401).json({ error: '用户名或密码错误' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check if user has a password (admin-created account)
|
|
164
|
+
if (!user.password_hash) {
|
|
165
|
+
return res.status(401).json({ error: '此账户不支持密码登录' });
|
|
93
166
|
}
|
|
94
167
|
|
|
95
168
|
// Check if user is disabled
|
|
96
169
|
if (user.status === 'disabled') {
|
|
97
|
-
return res.status(403).json({ error: '
|
|
170
|
+
return res.status(403).json({ error: '账户已被禁用' });
|
|
98
171
|
}
|
|
99
172
|
|
|
100
173
|
const isValidPassword = await bcrypt.compare(password, user.password_hash);
|
|
101
174
|
if (!isValidPassword) {
|
|
102
|
-
return res.status(401).json({ error: '
|
|
175
|
+
return res.status(401).json({ error: '用户名或密码错误' });
|
|
103
176
|
}
|
|
104
177
|
|
|
105
178
|
const token = generateToken(user);
|
|
@@ -110,6 +183,7 @@ router.post('/login', async (req, res) => {
|
|
|
110
183
|
user: {
|
|
111
184
|
id: user.id,
|
|
112
185
|
username: user.username,
|
|
186
|
+
email: user.email,
|
|
113
187
|
uuid: user.uuid,
|
|
114
188
|
role: user.role
|
|
115
189
|
},
|
|
@@ -127,7 +201,8 @@ router.get('/user', authenticateToken, (req, res) => {
|
|
|
127
201
|
res.json({
|
|
128
202
|
user: {
|
|
129
203
|
id: req.user.id,
|
|
130
|
-
username: req.user.username,
|
|
204
|
+
username: req.user.username || req.user.email,
|
|
205
|
+
email: req.user.email,
|
|
131
206
|
uuid: req.user.uuid,
|
|
132
207
|
role: req.user.role
|
|
133
208
|
}
|
|
@@ -141,4 +216,4 @@ router.post('/logout', authenticateToken, (req, res) => {
|
|
|
141
216
|
res.json({ success: true, message: 'Logged out successfully' });
|
|
142
217
|
});
|
|
143
218
|
|
|
144
|
-
export default router;
|
|
219
|
+
export default router;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
|
|
3
|
+
// SMTP configuration from environment variables
|
|
4
|
+
const getSmtpConfig = () => {
|
|
5
|
+
const config = {
|
|
6
|
+
host: process.env.SMTP_SERVER,
|
|
7
|
+
port: parseInt(process.env.SMTP_PORT) || 587,
|
|
8
|
+
secure: process.env.SMTP_USE_TLS === 'true', // true for 465, false for other ports
|
|
9
|
+
auth: {
|
|
10
|
+
user: process.env.SMTP_USERNAME,
|
|
11
|
+
pass: process.env.SMTP_PASSWORD,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Handle opportunistic TLS (STARTTLS)
|
|
16
|
+
if (process.env.SMTP_OPPORTUNISTIC_TLS === 'true') {
|
|
17
|
+
config.secure = false;
|
|
18
|
+
config.tls = {
|
|
19
|
+
rejectUnauthorized: false, // Allow self-signed certificates
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return config;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Check if SMTP is configured
|
|
27
|
+
export const isSmtpConfigured = () => {
|
|
28
|
+
return !!(process.env.SMTP_SERVER && process.env.SMTP_USERNAME && process.env.SMTP_PASSWORD);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Create transporter lazily (only when needed)
|
|
32
|
+
let transporter = null;
|
|
33
|
+
|
|
34
|
+
const getTransporter = () => {
|
|
35
|
+
if (!transporter) {
|
|
36
|
+
if (!isSmtpConfigured()) {
|
|
37
|
+
throw new Error('SMTP is not configured. Please set SMTP_SERVER, SMTP_USERNAME, and SMTP_PASSWORD environment variables.');
|
|
38
|
+
}
|
|
39
|
+
transporter = nodemailer.createTransport(getSmtpConfig());
|
|
40
|
+
}
|
|
41
|
+
return transporter;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Send verification code email
|
|
45
|
+
export const sendVerificationCode = async (email, code) => {
|
|
46
|
+
const transport = getTransporter();
|
|
47
|
+
const fromAddress = process.env.SMTP_FROM || process.env.SMTP_USERNAME;
|
|
48
|
+
|
|
49
|
+
const mailOptions = {
|
|
50
|
+
from: fromAddress,
|
|
51
|
+
to: email,
|
|
52
|
+
subject: 'AgentHub 登录验证码',
|
|
53
|
+
html: `
|
|
54
|
+
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
55
|
+
<div style="text-align: center; margin-bottom: 30px;">
|
|
56
|
+
<h1 style="color: #1a1a1a; font-size: 24px; margin: 0;">AgentHub</h1>
|
|
57
|
+
</div>
|
|
58
|
+
<div style="background-color: #f8f9fa; border-radius: 8px; padding: 30px; text-align: center;">
|
|
59
|
+
<h2 style="color: #1a1a1a; font-size: 20px; margin: 0 0 20px 0;">您的验证码</h2>
|
|
60
|
+
<div style="font-size: 36px; font-weight: bold; color: #2563eb; letter-spacing: 8px; margin: 20px 0; font-family: monospace;">
|
|
61
|
+
${code}
|
|
62
|
+
</div>
|
|
63
|
+
<p style="color: #666; font-size: 14px; margin: 20px 0 0 0;">
|
|
64
|
+
验证码有效期为 <strong>5 分钟</strong>,请尽快使用。
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; text-align: center;">
|
|
68
|
+
<p style="color: #999; font-size: 12px; margin: 0;">
|
|
69
|
+
如果您没有请求此验证码,请忽略此邮件。
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
`,
|
|
74
|
+
text: `您的 AgentHub 登录验证码是:${code},有效期 5 分钟。如果您没有请求此验证码,请忽略此邮件。`,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return transport.sendMail(mailOptions);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Verify SMTP connection
|
|
81
|
+
export const verifySmtpConnection = async () => {
|
|
82
|
+
try {
|
|
83
|
+
const transport = getTransporter();
|
|
84
|
+
await transport.verify();
|
|
85
|
+
return { success: true };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('SMTP connection verification failed:', error);
|
|
88
|
+
return { success: false, error: error.message };
|
|
89
|
+
}
|
|
90
|
+
};
|