@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.
@@ -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, db } from '../database/db.js';
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
- // User registration - first user becomes admin
25
- router.post('/register', async (req, res) => {
26
+ // Send verification code to email
27
+ router.post('/send-code', async (req, res) => {
26
28
  try {
27
- const { username, password } = req.body;
29
+ const { email } = req.body;
30
+ const ipAddress = req.ip || req.socket?.remoteAddress;
28
31
 
29
- if (!username || !password) {
30
- return res.status(400).json({ error: 'Username and password are required' });
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
- if (username.length < 3 || password.length < 6) {
34
- return res.status(400).json({
35
- error: 'Username must be at least 3 characters, password at least 6 characters'
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
- // Check if this is the first user (becomes admin)
40
- const userCount = userDb.getUserCount();
41
- const role = userCount === 0 ? 'admin' : 'user';
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
- // Hash password
45
- const saltRounds = 12;
46
- const passwordHash = await bcrypt.hash(password, saltRounds);
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
- // Create user with full details
49
- const user = userDb.createUserFull(username, passwordHash, uuid, role);
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
- // Initialize user directories (creates .claude.json with hasCompletedOnboarding=true)
52
- await initUserDirectories(uuid);
93
+ // Verify the code
94
+ const verification = verificationDb.verifyCode(email, code);
53
95
 
54
- // Generate token
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
- username: user.username,
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('Registration error:', error);
73
- if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
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: 'Username and password are required' });
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: 'Invalid username or password' });
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: 'Account has been disabled' });
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: 'Invalid username or password' });
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
+ };