agentdev-webui 1.0.0
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/lib/agent-api.js +530 -0
- package/lib/auth.js +127 -0
- package/lib/config.js +53 -0
- package/lib/database.js +762 -0
- package/lib/device-flow.js +257 -0
- package/lib/email.js +420 -0
- package/lib/encryption.js +112 -0
- package/lib/github.js +339 -0
- package/lib/history.js +143 -0
- package/lib/pwa.js +107 -0
- package/lib/redis-logs.js +226 -0
- package/lib/routes.js +680 -0
- package/migrations/000_create_database.sql +33 -0
- package/migrations/001_create_agentdev_schema.sql +135 -0
- package/migrations/001_create_agentdev_schema.sql.old +100 -0
- package/migrations/001_create_agentdev_schema_fixed.sql +135 -0
- package/migrations/002_add_github_token.sql +17 -0
- package/migrations/003_add_agent_logs_table.sql +23 -0
- package/migrations/004_remove_oauth_columns.sql +11 -0
- package/migrations/005_add_projects.sql +44 -0
- package/migrations/006_project_github_token.sql +7 -0
- package/migrations/007_project_repositories.sql +12 -0
- package/migrations/008_add_notifications.sql +20 -0
- package/migrations/009_unified_oauth.sql +153 -0
- package/migrations/README.md +97 -0
- package/package.json +37 -0
- package/public/css/styles.css +1140 -0
- package/public/device.html +384 -0
- package/public/docs.html +862 -0
- package/public/docs.md +697 -0
- package/public/favicon.svg +5 -0
- package/public/index.html +271 -0
- package/public/js/app.js +2379 -0
- package/public/login.html +224 -0
- package/public/profile.html +394 -0
- package/public/register.html +392 -0
- package/public/reset-password.html +349 -0
- package/public/verify-email.html +177 -0
- package/server.js +1450 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const jwt = require('jsonwebtoken');
|
|
3
|
+
const config = require('./config');
|
|
4
|
+
const db = require('./database');
|
|
5
|
+
const redisLogs = require('./redis-logs');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate a random user code (e.g., "ABCD-EFGH")
|
|
9
|
+
*/
|
|
10
|
+
function generateUserCode() {
|
|
11
|
+
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Remove confusing chars
|
|
12
|
+
let code = '';
|
|
13
|
+
for (let i = 0; i < 8; i++) {
|
|
14
|
+
if (i === 4) code += '-';
|
|
15
|
+
code += chars[Math.floor(Math.random() * chars.length)];
|
|
16
|
+
}
|
|
17
|
+
return code;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate a device code (64 hex chars)
|
|
22
|
+
*/
|
|
23
|
+
function generateDeviceCode() {
|
|
24
|
+
return crypto.randomBytes(32).toString('hex');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate JWT token for agent
|
|
29
|
+
*/
|
|
30
|
+
function generateAgentToken(agentId, userId, projectId = null) {
|
|
31
|
+
const payload = { agent_id: agentId, user_id: userId, type: 'agent' };
|
|
32
|
+
if (projectId) payload.project_id = projectId;
|
|
33
|
+
return jwt.sign(payload, config.JWT_SECRET, { expiresIn: config.JWT_EXPIRES_IN });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Verify JWT token
|
|
38
|
+
*/
|
|
39
|
+
function verifyAgentToken(token) {
|
|
40
|
+
try {
|
|
41
|
+
const decoded = jwt.verify(token, config.JWT_SECRET);
|
|
42
|
+
if (decoded.type !== 'agent') {
|
|
43
|
+
throw new Error('Invalid token type');
|
|
44
|
+
}
|
|
45
|
+
return decoded;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Hash token for storage
|
|
53
|
+
*/
|
|
54
|
+
function hashToken(token) {
|
|
55
|
+
return crypto.createHash('sha256').update(token).digest('hex');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Request device code
|
|
60
|
+
* Step 1 of device flow
|
|
61
|
+
*/
|
|
62
|
+
async function requestDeviceCode(agentName, capabilities) {
|
|
63
|
+
const deviceCode = generateDeviceCode();
|
|
64
|
+
const userCode = generateUserCode();
|
|
65
|
+
|
|
66
|
+
// Store in database
|
|
67
|
+
await db.createDeviceCode(deviceCode, userCode, agentName, capabilities);
|
|
68
|
+
|
|
69
|
+
// Also store in Redis for quick lookup with TTL
|
|
70
|
+
await redisLogs.set(
|
|
71
|
+
`device:${deviceCode}`,
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
user_code: userCode,
|
|
74
|
+
agent_name: agentName,
|
|
75
|
+
capabilities,
|
|
76
|
+
status: 'pending'
|
|
77
|
+
}),
|
|
78
|
+
config.DEVICE_CODE_TTL
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await redisLogs.set(
|
|
82
|
+
`usercode:${userCode}`,
|
|
83
|
+
deviceCode,
|
|
84
|
+
config.DEVICE_CODE_TTL
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
device_code: deviceCode,
|
|
89
|
+
user_code: userCode,
|
|
90
|
+
verification_uri: `${config.BASE_URL}/device`,
|
|
91
|
+
expires_in: config.DEVICE_CODE_TTL,
|
|
92
|
+
interval: config.DEVICE_CODE_POLL_INTERVAL
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get device code details by user code
|
|
98
|
+
*/
|
|
99
|
+
async function getDeviceCodeByUserCode(userCode) {
|
|
100
|
+
// Try Redis first (faster)
|
|
101
|
+
const deviceCode = await redisLogs.get(`usercode:${userCode}`);
|
|
102
|
+
if (deviceCode) {
|
|
103
|
+
const data = await redisLogs.get(`device:${deviceCode}`);
|
|
104
|
+
if (data) {
|
|
105
|
+
return { device_code: deviceCode, ...data };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Fallback to database
|
|
110
|
+
const dbRecord = await db.getDeviceCodeByUserCode(userCode);
|
|
111
|
+
if (dbRecord) {
|
|
112
|
+
return {
|
|
113
|
+
device_code: dbRecord.device_code,
|
|
114
|
+
user_code: dbRecord.user_code,
|
|
115
|
+
agent_name: dbRecord.agent_name,
|
|
116
|
+
capabilities: dbRecord.agent_capabilities,
|
|
117
|
+
status: dbRecord.status,
|
|
118
|
+
user_id: dbRecord.user_id
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Approve device code
|
|
127
|
+
* User approves agent registration in Web UI
|
|
128
|
+
*/
|
|
129
|
+
async function approveDeviceCode(userCode, userId, projectId = null) {
|
|
130
|
+
const deviceData = await getDeviceCodeByUserCode(userCode);
|
|
131
|
+
if (!deviceData) {
|
|
132
|
+
throw new Error('Device code not found or expired');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (deviceData.status !== 'pending') {
|
|
136
|
+
throw new Error('Device code already processed');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Update database
|
|
140
|
+
await db.approveDeviceCode(deviceData.device_code, userId);
|
|
141
|
+
|
|
142
|
+
// Update Redis (include project_id)
|
|
143
|
+
await redisLogs.set(
|
|
144
|
+
`device:${deviceData.device_code}`,
|
|
145
|
+
JSON.stringify({
|
|
146
|
+
...deviceData,
|
|
147
|
+
status: 'approved',
|
|
148
|
+
user_id: userId,
|
|
149
|
+
project_id: projectId
|
|
150
|
+
}),
|
|
151
|
+
300 // Keep for 5 more minutes for polling
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Deny device code
|
|
159
|
+
*/
|
|
160
|
+
async function denyDeviceCode(userCode) {
|
|
161
|
+
const deviceData = await getDeviceCodeByUserCode(userCode);
|
|
162
|
+
if (!deviceData) {
|
|
163
|
+
throw new Error('Device code not found or expired');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Update database
|
|
167
|
+
await db.denyDeviceCode(deviceData.device_code);
|
|
168
|
+
|
|
169
|
+
// Update Redis
|
|
170
|
+
await redisLogs.set(
|
|
171
|
+
`device:${deviceData.device_code}`,
|
|
172
|
+
JSON.stringify({
|
|
173
|
+
...deviceData,
|
|
174
|
+
status: 'denied'
|
|
175
|
+
}),
|
|
176
|
+
60 // Keep for 1 minute
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Poll for device token
|
|
184
|
+
* Step 2 of device flow - agent polls this endpoint
|
|
185
|
+
*/
|
|
186
|
+
async function pollDeviceToken(deviceCode) {
|
|
187
|
+
// Check Redis first
|
|
188
|
+
let deviceData = await redisLogs.get(`device:${deviceCode}`);
|
|
189
|
+
|
|
190
|
+
// Fallback to database
|
|
191
|
+
if (!deviceData) {
|
|
192
|
+
const dbRecord = await db.getDeviceCode(deviceCode);
|
|
193
|
+
if (!dbRecord) {
|
|
194
|
+
return { error: 'invalid_grant', error_description: 'Device code not found or expired' };
|
|
195
|
+
}
|
|
196
|
+
deviceData = {
|
|
197
|
+
status: dbRecord.status,
|
|
198
|
+
user_id: dbRecord.user_id,
|
|
199
|
+
agent_name: dbRecord.agent_name,
|
|
200
|
+
capabilities: dbRecord.agent_capabilities
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (deviceData.status === 'pending') {
|
|
205
|
+
return { error: 'authorization_pending', error_description: 'User has not approved yet' };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (deviceData.status === 'denied') {
|
|
209
|
+
return { error: 'access_denied', error_description: 'User denied the request' };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (deviceData.status === 'approved') {
|
|
213
|
+
// Generate agent ID and token
|
|
214
|
+
const agentId = `agent-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
215
|
+
const projectId = deviceData.project_id || null;
|
|
216
|
+
const accessToken = generateAgentToken(agentId, deviceData.user_id, projectId);
|
|
217
|
+
const tokenHash = hashToken(accessToken);
|
|
218
|
+
|
|
219
|
+
// Create agent in database
|
|
220
|
+
await db.createAgent({
|
|
221
|
+
id: agentId,
|
|
222
|
+
userId: deviceData.user_id,
|
|
223
|
+
name: deviceData.agent_name,
|
|
224
|
+
hostname: null,
|
|
225
|
+
capabilities: deviceData.capabilities,
|
|
226
|
+
accessTokenHash: tokenHash,
|
|
227
|
+
projectId: projectId
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Clean up device code
|
|
231
|
+
await redisLogs.del(`device:${deviceCode}`);
|
|
232
|
+
await redisLogs.del(`usercode:${deviceData.user_code}`);
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
access_token: accessToken,
|
|
236
|
+
token_type: 'Bearer',
|
|
237
|
+
expires_in: 90 * 24 * 60 * 60, // 90 days in seconds
|
|
238
|
+
agent_id: agentId,
|
|
239
|
+
project_id: projectId
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { error: 'invalid_grant', error_description: 'Invalid device code state' };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = {
|
|
247
|
+
generateUserCode,
|
|
248
|
+
generateDeviceCode,
|
|
249
|
+
generateAgentToken,
|
|
250
|
+
verifyAgentToken,
|
|
251
|
+
hashToken,
|
|
252
|
+
requestDeviceCode,
|
|
253
|
+
getDeviceCodeByUserCode,
|
|
254
|
+
approveDeviceCode,
|
|
255
|
+
denyDeviceCode,
|
|
256
|
+
pollDeviceToken
|
|
257
|
+
};
|
package/lib/email.js
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
const nodemailer = require('nodemailer');
|
|
2
|
+
|
|
3
|
+
// SMTP configuration from environment or config
|
|
4
|
+
const SMTP_CONFIG = {
|
|
5
|
+
host: process.env.SMTP_HOST || 'smtp.gmail.com',
|
|
6
|
+
port: parseInt(process.env.SMTP_PORT || '587'),
|
|
7
|
+
secure: process.env.SMTP_SECURE === 'true', // use TLS for port 587
|
|
8
|
+
auth: {
|
|
9
|
+
user: process.env.SMTP_USER || 'riccardo.ravaro@datatamer.ai',
|
|
10
|
+
pass: process.env.SMTP_PASSWORD || ''
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Create reusable transporter
|
|
15
|
+
let transporter = null;
|
|
16
|
+
|
|
17
|
+
function getTransporter() {
|
|
18
|
+
if (!transporter) {
|
|
19
|
+
transporter = nodemailer.createTransport(SMTP_CONFIG);
|
|
20
|
+
}
|
|
21
|
+
return transporter;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function sendEmail(to, subject, html) {
|
|
25
|
+
try {
|
|
26
|
+
const transport = getTransporter();
|
|
27
|
+
|
|
28
|
+
const info = await transport.sendMail({
|
|
29
|
+
from: `"AgentDev" <${SMTP_CONFIG.auth.user}>`,
|
|
30
|
+
to,
|
|
31
|
+
subject,
|
|
32
|
+
html
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
console.log('Email sent:', info.messageId);
|
|
36
|
+
return { success: true, messageId: info.messageId };
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error sending email:', error);
|
|
39
|
+
return { success: false, error: error.message };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function sendPasswordResetEmail(email, resetToken, baseUrl) {
|
|
44
|
+
const resetLink = `${baseUrl}/reset-password?token=${resetToken}`;
|
|
45
|
+
|
|
46
|
+
const html = `
|
|
47
|
+
<!DOCTYPE html>
|
|
48
|
+
<html>
|
|
49
|
+
<head>
|
|
50
|
+
<meta charset="UTF-8">
|
|
51
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
52
|
+
<style>
|
|
53
|
+
body {
|
|
54
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
55
|
+
line-height: 1.6;
|
|
56
|
+
color: #333;
|
|
57
|
+
margin: 0;
|
|
58
|
+
padding: 0;
|
|
59
|
+
background-color: #f5f5f5;
|
|
60
|
+
}
|
|
61
|
+
.container {
|
|
62
|
+
max-width: 600px;
|
|
63
|
+
margin: 40px auto;
|
|
64
|
+
background: white;
|
|
65
|
+
border-radius: 8px;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
68
|
+
}
|
|
69
|
+
.header {
|
|
70
|
+
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
|
71
|
+
padding: 40px 30px;
|
|
72
|
+
text-align: center;
|
|
73
|
+
}
|
|
74
|
+
.header h1 {
|
|
75
|
+
margin: 0;
|
|
76
|
+
color: white;
|
|
77
|
+
font-size: 28px;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
}
|
|
80
|
+
.content {
|
|
81
|
+
padding: 40px 30px;
|
|
82
|
+
}
|
|
83
|
+
.content p {
|
|
84
|
+
margin: 0 0 20px;
|
|
85
|
+
font-size: 16px;
|
|
86
|
+
}
|
|
87
|
+
.button {
|
|
88
|
+
display: inline-block;
|
|
89
|
+
padding: 14px 32px;
|
|
90
|
+
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
|
91
|
+
color: white !important;
|
|
92
|
+
text-decoration: none;
|
|
93
|
+
border-radius: 6px;
|
|
94
|
+
font-weight: 600;
|
|
95
|
+
font-size: 16px;
|
|
96
|
+
margin: 20px 0;
|
|
97
|
+
}
|
|
98
|
+
.button:hover {
|
|
99
|
+
background: linear-gradient(135deg, #ee5a6f 0%, #ff6b6b 100%);
|
|
100
|
+
}
|
|
101
|
+
.code-box {
|
|
102
|
+
background: #f8f9fa;
|
|
103
|
+
border: 1px solid #e9ecef;
|
|
104
|
+
border-radius: 6px;
|
|
105
|
+
padding: 16px;
|
|
106
|
+
margin: 20px 0;
|
|
107
|
+
text-align: center;
|
|
108
|
+
}
|
|
109
|
+
.code {
|
|
110
|
+
font-family: 'Courier New', monospace;
|
|
111
|
+
font-size: 18px;
|
|
112
|
+
font-weight: bold;
|
|
113
|
+
color: #333;
|
|
114
|
+
letter-spacing: 2px;
|
|
115
|
+
}
|
|
116
|
+
.footer {
|
|
117
|
+
background: #f8f9fa;
|
|
118
|
+
padding: 30px;
|
|
119
|
+
text-align: center;
|
|
120
|
+
font-size: 14px;
|
|
121
|
+
color: #6c757d;
|
|
122
|
+
}
|
|
123
|
+
.warning {
|
|
124
|
+
background: #fff3cd;
|
|
125
|
+
border: 1px solid #ffeeba;
|
|
126
|
+
border-radius: 6px;
|
|
127
|
+
padding: 16px;
|
|
128
|
+
margin: 20px 0;
|
|
129
|
+
font-size: 14px;
|
|
130
|
+
color: #856404;
|
|
131
|
+
}
|
|
132
|
+
</style>
|
|
133
|
+
</head>
|
|
134
|
+
<body>
|
|
135
|
+
<div class="container">
|
|
136
|
+
<div class="header">
|
|
137
|
+
<h1>🔒 Password Reset</h1>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="content">
|
|
141
|
+
<p>Hello,</p>
|
|
142
|
+
|
|
143
|
+
<p>We received a request to reset your password for your AgentDev account. Click the button below to create a new password:</p>
|
|
144
|
+
|
|
145
|
+
<div style="text-align: center;">
|
|
146
|
+
<a href="${resetLink}" class="button">Reset Password</a>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<p style="font-size: 14px; color: #6c757d;">Or copy and paste this link into your browser:</p>
|
|
150
|
+
<div class="code-box">
|
|
151
|
+
<div class="code">${resetLink}</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div class="warning">
|
|
155
|
+
<strong>⚠️ Security Notice:</strong>
|
|
156
|
+
<ul style="margin: 8px 0; padding-left: 20px;">
|
|
157
|
+
<li>This link expires in 1 hour</li>
|
|
158
|
+
<li>If you didn't request this, you can safely ignore this email</li>
|
|
159
|
+
<li>Your password won't change until you click the link and create a new one</li>
|
|
160
|
+
</ul>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<p>If you have any questions or concerns, please contact support.</p>
|
|
164
|
+
|
|
165
|
+
<p>Best regards,<br>
|
|
166
|
+
<strong>The AgentDev Team</strong></p>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div class="footer">
|
|
170
|
+
<p>This is an automated email from AgentDev. Please do not reply to this email.</p>
|
|
171
|
+
<p style="margin-top: 8px;">© ${new Date().getFullYear()} AgentDev. All rights reserved.</p>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</body>
|
|
175
|
+
</html>
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
return await sendEmail(
|
|
179
|
+
email,
|
|
180
|
+
'🔒 Reset your AgentDev password',
|
|
181
|
+
html
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function sendWelcomeEmail(email, baseUrl) {
|
|
186
|
+
const html = `
|
|
187
|
+
<!DOCTYPE html>
|
|
188
|
+
<html>
|
|
189
|
+
<head>
|
|
190
|
+
<meta charset="UTF-8">
|
|
191
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
192
|
+
<style>
|
|
193
|
+
body {
|
|
194
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
195
|
+
line-height: 1.6;
|
|
196
|
+
color: #333;
|
|
197
|
+
margin: 0;
|
|
198
|
+
padding: 0;
|
|
199
|
+
background-color: #f5f5f5;
|
|
200
|
+
}
|
|
201
|
+
.container {
|
|
202
|
+
max-width: 600px;
|
|
203
|
+
margin: 40px auto;
|
|
204
|
+
background: white;
|
|
205
|
+
border-radius: 8px;
|
|
206
|
+
overflow: hidden;
|
|
207
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
208
|
+
}
|
|
209
|
+
.header {
|
|
210
|
+
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
|
211
|
+
padding: 40px 30px;
|
|
212
|
+
text-align: center;
|
|
213
|
+
}
|
|
214
|
+
.header h1 {
|
|
215
|
+
margin: 0;
|
|
216
|
+
color: white;
|
|
217
|
+
font-size: 28px;
|
|
218
|
+
font-weight: 600;
|
|
219
|
+
}
|
|
220
|
+
.content {
|
|
221
|
+
padding: 40px 30px;
|
|
222
|
+
}
|
|
223
|
+
.button {
|
|
224
|
+
display: inline-block;
|
|
225
|
+
padding: 14px 32px;
|
|
226
|
+
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
|
227
|
+
color: white !important;
|
|
228
|
+
text-decoration: none;
|
|
229
|
+
border-radius: 6px;
|
|
230
|
+
font-weight: 600;
|
|
231
|
+
font-size: 16px;
|
|
232
|
+
margin: 20px 0;
|
|
233
|
+
}
|
|
234
|
+
.footer {
|
|
235
|
+
background: #f8f9fa;
|
|
236
|
+
padding: 30px;
|
|
237
|
+
text-align: center;
|
|
238
|
+
font-size: 14px;
|
|
239
|
+
color: #6c757d;
|
|
240
|
+
}
|
|
241
|
+
</style>
|
|
242
|
+
</head>
|
|
243
|
+
<body>
|
|
244
|
+
<div class="container">
|
|
245
|
+
<div class="header">
|
|
246
|
+
<h1>🎉 Welcome to AgentDev!</h1>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<div class="content">
|
|
250
|
+
<p>Hello,</p>
|
|
251
|
+
|
|
252
|
+
<p>Your AgentDev account has been created successfully! You can now start managing distributed agents and automating your workflows.</p>
|
|
253
|
+
|
|
254
|
+
<div style="text-align: center;">
|
|
255
|
+
<a href="${baseUrl}/login" class="button">Go to Dashboard</a>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<p><strong>Getting Started:</strong></p>
|
|
259
|
+
<ol>
|
|
260
|
+
<li>Configure your GitHub OAuth App in the Profile settings</li>
|
|
261
|
+
<li>Install the agent client on your machines</li>
|
|
262
|
+
<li>Register agents using the device flow</li>
|
|
263
|
+
<li>Start automating your GitHub tickets!</li>
|
|
264
|
+
</ol>
|
|
265
|
+
|
|
266
|
+
<p>If you have any questions, feel free to reach out to our support team.</p>
|
|
267
|
+
|
|
268
|
+
<p>Happy automating!<br>
|
|
269
|
+
<strong>The AgentDev Team</strong></p>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div class="footer">
|
|
273
|
+
<p>© ${new Date().getFullYear()} AgentDev. All rights reserved.</p>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</body>
|
|
277
|
+
</html>
|
|
278
|
+
`;
|
|
279
|
+
|
|
280
|
+
return await sendEmail(
|
|
281
|
+
email,
|
|
282
|
+
'🎉 Welcome to AgentDev!',
|
|
283
|
+
html
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function sendVerificationEmail(email, verificationToken, baseUrl) {
|
|
288
|
+
const verifyLink = `${baseUrl}/verify-email?token=${verificationToken}`;
|
|
289
|
+
|
|
290
|
+
const html = `
|
|
291
|
+
<!DOCTYPE html>
|
|
292
|
+
<html>
|
|
293
|
+
<head>
|
|
294
|
+
<meta charset="UTF-8">
|
|
295
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
296
|
+
<style>
|
|
297
|
+
body {
|
|
298
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
299
|
+
line-height: 1.6;
|
|
300
|
+
color: #333;
|
|
301
|
+
margin: 0;
|
|
302
|
+
padding: 0;
|
|
303
|
+
background-color: #f5f5f5;
|
|
304
|
+
}
|
|
305
|
+
.container {
|
|
306
|
+
max-width: 600px;
|
|
307
|
+
margin: 40px auto;
|
|
308
|
+
background: white;
|
|
309
|
+
border-radius: 8px;
|
|
310
|
+
overflow: hidden;
|
|
311
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
312
|
+
}
|
|
313
|
+
.header {
|
|
314
|
+
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
|
315
|
+
padding: 40px 30px;
|
|
316
|
+
text-align: center;
|
|
317
|
+
}
|
|
318
|
+
.header h1 {
|
|
319
|
+
margin: 0;
|
|
320
|
+
color: white;
|
|
321
|
+
font-size: 28px;
|
|
322
|
+
font-weight: 600;
|
|
323
|
+
}
|
|
324
|
+
.content {
|
|
325
|
+
padding: 40px 30px;
|
|
326
|
+
}
|
|
327
|
+
.button {
|
|
328
|
+
display: inline-block;
|
|
329
|
+
padding: 14px 32px;
|
|
330
|
+
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
|
331
|
+
color: white !important;
|
|
332
|
+
text-decoration: none;
|
|
333
|
+
border-radius: 6px;
|
|
334
|
+
font-weight: 600;
|
|
335
|
+
font-size: 16px;
|
|
336
|
+
margin: 20px 0;
|
|
337
|
+
}
|
|
338
|
+
.code-box {
|
|
339
|
+
background: #f8f9fa;
|
|
340
|
+
border: 1px solid #e9ecef;
|
|
341
|
+
border-radius: 6px;
|
|
342
|
+
padding: 16px;
|
|
343
|
+
margin: 20px 0;
|
|
344
|
+
text-align: center;
|
|
345
|
+
}
|
|
346
|
+
.footer {
|
|
347
|
+
background: #f8f9fa;
|
|
348
|
+
padding: 30px;
|
|
349
|
+
text-align: center;
|
|
350
|
+
font-size: 14px;
|
|
351
|
+
color: #6c757d;
|
|
352
|
+
}
|
|
353
|
+
.warning {
|
|
354
|
+
background: #fff3cd;
|
|
355
|
+
border: 1px solid #ffeeba;
|
|
356
|
+
border-radius: 6px;
|
|
357
|
+
padding: 16px;
|
|
358
|
+
margin: 20px 0;
|
|
359
|
+
font-size: 14px;
|
|
360
|
+
color: #856404;
|
|
361
|
+
}
|
|
362
|
+
</style>
|
|
363
|
+
</head>
|
|
364
|
+
<body>
|
|
365
|
+
<div class="container">
|
|
366
|
+
<div class="header">
|
|
367
|
+
<h1>📧 Verify Your Email</h1>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<div class="content">
|
|
371
|
+
<p>Hello,</p>
|
|
372
|
+
|
|
373
|
+
<p>Thank you for registering with AgentDev! Please verify your email address to activate your account.</p>
|
|
374
|
+
|
|
375
|
+
<div style="text-align: center;">
|
|
376
|
+
<a href="${verifyLink}" class="button">Verify Email Address</a>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<p style="font-size: 14px; color: #6c757d;">Or copy and paste this link into your browser:</p>
|
|
380
|
+
<div class="code-box">
|
|
381
|
+
<div style="font-family: 'Courier New', monospace; font-size: 12px; word-break: break-all;">${verifyLink}</div>
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<div class="warning">
|
|
385
|
+
<strong>⏰ Important:</strong>
|
|
386
|
+
<ul style="margin: 8px 0; padding-left: 20px;">
|
|
387
|
+
<li>This link expires in 24 hours</li>
|
|
388
|
+
<li>You must verify your email before you can log in</li>
|
|
389
|
+
<li>If you didn't create this account, you can safely ignore this email</li>
|
|
390
|
+
</ul>
|
|
391
|
+
</div>
|
|
392
|
+
|
|
393
|
+
<p>Once verified, you'll be able to sign in and start managing your distributed agents!</p>
|
|
394
|
+
|
|
395
|
+
<p>Best regards,<br>
|
|
396
|
+
<strong>The AgentDev Team</strong></p>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
<div class="footer">
|
|
400
|
+
<p>This is an automated email from AgentDev. Please do not reply to this email.</p>
|
|
401
|
+
<p style="margin-top: 8px;">© ${new Date().getFullYear()} AgentDev. All rights reserved.</p>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
</body>
|
|
405
|
+
</html>
|
|
406
|
+
`;
|
|
407
|
+
|
|
408
|
+
return await sendEmail(
|
|
409
|
+
email,
|
|
410
|
+
'📧 Verify your AgentDev email address',
|
|
411
|
+
html
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
module.exports = {
|
|
416
|
+
sendEmail,
|
|
417
|
+
sendPasswordResetEmail,
|
|
418
|
+
sendWelcomeEmail,
|
|
419
|
+
sendVerificationEmail
|
|
420
|
+
};
|