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
package/lib/routes.js
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
const agentApi = require('./agent-api');
|
|
2
|
+
const deviceFlow = require('./device-flow');
|
|
3
|
+
const db = require('./database');
|
|
4
|
+
const encryption = require('./encryption');
|
|
5
|
+
const auth = require('./auth');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register all API routes
|
|
9
|
+
*/
|
|
10
|
+
function registerRoutes(req, res) {
|
|
11
|
+
const url = req.url.split('?')[0];
|
|
12
|
+
const method = req.method;
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Agent API Routes (require JWT authentication)
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
if (url === '/api/agent/device/code' && method === 'POST') {
|
|
19
|
+
return agentApi.handleDeviceCodeRequest(req, res);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (url === '/api/agent/device/token' && method === 'POST') {
|
|
23
|
+
return agentApi.handleDeviceTokenPoll(req, res);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (url === '/api/agent/heartbeat' && method === 'POST') {
|
|
27
|
+
return agentApi.handleHeartbeat(req, res);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (url === '/api/agent/work' && method === 'GET') {
|
|
31
|
+
return agentApi.handleWorkRequest(req, res);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (url === '/api/agent/logs' && method === 'POST') {
|
|
35
|
+
return agentApi.handleLogUpload(req, res);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (url === '/api/agent/complete' && method === 'POST') {
|
|
39
|
+
return agentApi.handleWorkComplete(req, res);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (url === '/api/agent/oauth' && method === 'GET') {
|
|
43
|
+
return agentApi.handleOAuthRequest(req, res);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (url === '/api/agent/ensure-ticket' && method === 'POST') {
|
|
47
|
+
return agentApi.handleEnsureTicket(req, res);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (url === '/api/agent/should-stop' && method === 'GET') {
|
|
51
|
+
return agentApi.handleShouldStop(req, res);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Project board configuration (no auth — agents need this during registration)
|
|
55
|
+
if (url === '/api/agent/project-config' && method === 'GET') {
|
|
56
|
+
const config = require('./config');
|
|
57
|
+
const urlObj = new URL(req.url, `http://${req.headers.host}`);
|
|
58
|
+
const projectId = urlObj.searchParams.get('project_id');
|
|
59
|
+
|
|
60
|
+
(async () => {
|
|
61
|
+
try {
|
|
62
|
+
let projectData = null;
|
|
63
|
+
if (projectId) {
|
|
64
|
+
projectData = await db.getProjectById(parseInt(projectId));
|
|
65
|
+
}
|
|
66
|
+
if (!projectData) {
|
|
67
|
+
// Try project 1 from DB, fallback to config
|
|
68
|
+
projectData = await db.getProjectById(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (projectData) {
|
|
72
|
+
const statusOptions = typeof projectData.status_options === 'string'
|
|
73
|
+
? JSON.parse(projectData.status_options)
|
|
74
|
+
: projectData.status_options;
|
|
75
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
76
|
+
res.end(JSON.stringify({
|
|
77
|
+
github_org: projectData.github_org,
|
|
78
|
+
project_id: projectData.github_project_id,
|
|
79
|
+
project_number: projectData.project_number,
|
|
80
|
+
status_field_id: projectData.status_field_id,
|
|
81
|
+
status_options: statusOptions
|
|
82
|
+
}));
|
|
83
|
+
} else {
|
|
84
|
+
// Fallback to hardcoded config
|
|
85
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
86
|
+
res.end(JSON.stringify({
|
|
87
|
+
github_org: config.GITHUB_ORG,
|
|
88
|
+
project_id: config.PROJECT_ID,
|
|
89
|
+
project_number: config.PROJECT_NUMBER,
|
|
90
|
+
status_field_id: config.STATUS_FIELD_ID,
|
|
91
|
+
status_options: config.STATUS_OPTIONS
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Project config error:', error);
|
|
96
|
+
// Fallback to hardcoded config
|
|
97
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
98
|
+
res.end(JSON.stringify({
|
|
99
|
+
github_org: config.GITHUB_ORG,
|
|
100
|
+
project_id: config.PROJECT_ID,
|
|
101
|
+
project_number: config.PROJECT_NUMBER,
|
|
102
|
+
status_field_id: config.STATUS_FIELD_ID,
|
|
103
|
+
status_options: config.STATUS_OPTIONS
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
})();
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// Device Authorization Routes (require user session)
|
|
112
|
+
// ============================================================================
|
|
113
|
+
|
|
114
|
+
if (url === '/api/device/lookup' && method === 'POST') {
|
|
115
|
+
if (!auth.isAuthenticated(req)) {
|
|
116
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
117
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let body = '';
|
|
122
|
+
req.on('data', chunk => body += chunk);
|
|
123
|
+
req.on('end', async () => {
|
|
124
|
+
try {
|
|
125
|
+
const { user_code } = JSON.parse(body);
|
|
126
|
+
|
|
127
|
+
const deviceData = await deviceFlow.getDeviceCodeByUserCode(user_code);
|
|
128
|
+
|
|
129
|
+
if (!deviceData) {
|
|
130
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
131
|
+
res.end(JSON.stringify({ error: 'Invalid or expired code' }));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
136
|
+
res.end(JSON.stringify(deviceData));
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('Device lookup error:', error);
|
|
139
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
140
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (url === '/api/device/approve' && method === 'POST') {
|
|
147
|
+
if (!auth.isAuthenticated(req)) {
|
|
148
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
149
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let body = '';
|
|
154
|
+
req.on('data', chunk => body += chunk);
|
|
155
|
+
req.on('end', async () => {
|
|
156
|
+
try {
|
|
157
|
+
const { user_code } = JSON.parse(body);
|
|
158
|
+
|
|
159
|
+
// Get user ID from session
|
|
160
|
+
// For now, use default user ID 1 (would need proper user session management)
|
|
161
|
+
const userId = 1;
|
|
162
|
+
|
|
163
|
+
await deviceFlow.approveDeviceCode(user_code, userId);
|
|
164
|
+
|
|
165
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
166
|
+
res.end(JSON.stringify({ success: true }));
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('Device approve error:', error);
|
|
169
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
170
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (url === '/api/device/deny' && method === 'POST') {
|
|
177
|
+
if (!auth.isAuthenticated(req)) {
|
|
178
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
179
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let body = '';
|
|
184
|
+
req.on('data', chunk => body += chunk);
|
|
185
|
+
req.on('end', async () => {
|
|
186
|
+
try {
|
|
187
|
+
const { user_code } = JSON.parse(body);
|
|
188
|
+
|
|
189
|
+
await deviceFlow.denyDeviceCode(user_code);
|
|
190
|
+
|
|
191
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
192
|
+
res.end(JSON.stringify({ success: true }));
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error('Device deny error:', error);
|
|
195
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
196
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// Profile Routes (require user session)
|
|
204
|
+
// ============================================================================
|
|
205
|
+
|
|
206
|
+
if (url === '/api/profile' && method === 'GET') {
|
|
207
|
+
if (!auth.isAuthenticated(req)) {
|
|
208
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
209
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
(async () => {
|
|
214
|
+
try {
|
|
215
|
+
// Get user from session (for now, use default user ID 1)
|
|
216
|
+
const userId = 1;
|
|
217
|
+
const user = await db.getUserById(userId);
|
|
218
|
+
|
|
219
|
+
if (!user) {
|
|
220
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
221
|
+
res.end(JSON.stringify({ error: 'User not found' }));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Get user's OAuth providers from unified table
|
|
226
|
+
const oauthProviders = await db.getUserOAuthProviders(userId);
|
|
227
|
+
|
|
228
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
229
|
+
res.end(JSON.stringify({
|
|
230
|
+
email: user.email,
|
|
231
|
+
max_agents: user.max_agents,
|
|
232
|
+
oauth_providers: oauthProviders.map(p => ({
|
|
233
|
+
provider_key: p.provider_key,
|
|
234
|
+
provider_name: p.provider_name,
|
|
235
|
+
status: p.status,
|
|
236
|
+
scopes: p.scopes,
|
|
237
|
+
has_token: !!p.access_token
|
|
238
|
+
})),
|
|
239
|
+
has_github_token: oauthProviders.some(p => p.provider_key === 'github' && p.access_token) || !!user.github_token_encrypted
|
|
240
|
+
}));
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error('Profile get error:', error);
|
|
243
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
244
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
245
|
+
}
|
|
246
|
+
})();
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (url === '/api/profile/limits' && method === 'POST') {
|
|
251
|
+
if (!auth.isAuthenticated(req)) {
|
|
252
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
253
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let body = '';
|
|
258
|
+
req.on('data', chunk => body += chunk);
|
|
259
|
+
req.on('end', async () => {
|
|
260
|
+
try {
|
|
261
|
+
const { max_agents } = JSON.parse(body);
|
|
262
|
+
|
|
263
|
+
// Update user (for now, use default user ID 1)
|
|
264
|
+
const userId = 1;
|
|
265
|
+
await db.query(
|
|
266
|
+
'UPDATE agentdev_users SET max_agents = $1 WHERE id = $2',
|
|
267
|
+
[max_agents, userId]
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
271
|
+
res.end(JSON.stringify({ success: true }));
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error('Limits update error:', error);
|
|
274
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
275
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (url === '/api/agents' && method === 'GET') {
|
|
282
|
+
if (!auth.isAuthenticated(req)) {
|
|
283
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
284
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
(async () => {
|
|
289
|
+
try {
|
|
290
|
+
// Get user agents (for now, use default user ID 1)
|
|
291
|
+
const userId = 1;
|
|
292
|
+
const agents = await db.getAgentsByUser(userId);
|
|
293
|
+
|
|
294
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
295
|
+
res.end(JSON.stringify(agents));
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error('Agents list error:', error);
|
|
298
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
299
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
300
|
+
}
|
|
301
|
+
})();
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ============================================================================
|
|
306
|
+
// Project Routes (require user session)
|
|
307
|
+
// ============================================================================
|
|
308
|
+
|
|
309
|
+
if (url === '/api/projects' && method === 'GET') {
|
|
310
|
+
if (!auth.isAuthenticated(req)) {
|
|
311
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
312
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
(async () => {
|
|
317
|
+
try {
|
|
318
|
+
const projects = await db.getProjects();
|
|
319
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
320
|
+
res.end(JSON.stringify(projects));
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error('Projects list error:', error);
|
|
323
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
324
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
325
|
+
}
|
|
326
|
+
})();
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (url === '/api/projects' && method === 'POST') {
|
|
331
|
+
if (!auth.isAuthenticated(req)) {
|
|
332
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
333
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let body = '';
|
|
338
|
+
req.on('data', chunk => body += chunk);
|
|
339
|
+
req.on('end', async () => {
|
|
340
|
+
try {
|
|
341
|
+
const data = JSON.parse(body);
|
|
342
|
+
if (!data.name || !data.github_org || !data.project_number || !data.github_project_id || !data.status_field_id) {
|
|
343
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
344
|
+
res.end(JSON.stringify({ error: 'Missing required fields: name, github_org, project_number, github_project_id, status_field_id' }));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// If save_token_globally, save the token to unified OAuth table
|
|
349
|
+
if (data.save_token_globally && data.github_token) {
|
|
350
|
+
const sessionId = auth.getSessionFromRequest(req);
|
|
351
|
+
const session = auth.getSession(sessionId);
|
|
352
|
+
if (session?.userId) {
|
|
353
|
+
const encryptedToken = encryption.encrypt(data.github_token);
|
|
354
|
+
// Save to unified OAuth providers table
|
|
355
|
+
const githubConfig = await db.getOAuthProviderConfig('github');
|
|
356
|
+
if (githubConfig) {
|
|
357
|
+
await db.upsertOAuthProvider({
|
|
358
|
+
userId: session.userId,
|
|
359
|
+
configId: githubConfig.id,
|
|
360
|
+
providerKey: 'github',
|
|
361
|
+
accessToken: encryptedToken,
|
|
362
|
+
scopes: ['repo', 'read:org', 'project']
|
|
363
|
+
});
|
|
364
|
+
console.log(`Saved GitHub token to unified OAuth table for user ${session.userId}`);
|
|
365
|
+
}
|
|
366
|
+
// Also save to legacy column for backward compatibility
|
|
367
|
+
await db.updateUserGitHubToken(session.userId, encryptedToken);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
delete data.save_token_globally;
|
|
371
|
+
|
|
372
|
+
const project = await db.createProject(data);
|
|
373
|
+
// Don't leak token back to frontend
|
|
374
|
+
const { github_token: _token, ...safeProject } = project;
|
|
375
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
376
|
+
res.end(JSON.stringify(safeProject));
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error('Project create error:', error);
|
|
379
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
380
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// PUT /api/projects/:id
|
|
387
|
+
const projectUpdateMatch = url.match(/^\/api\/projects\/(\d+)$/);
|
|
388
|
+
if (projectUpdateMatch && method === 'PUT') {
|
|
389
|
+
if (!auth.isAuthenticated(req)) {
|
|
390
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
391
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const projectId = parseInt(projectUpdateMatch[1]);
|
|
396
|
+
let body = '';
|
|
397
|
+
req.on('data', chunk => body += chunk);
|
|
398
|
+
req.on('end', async () => {
|
|
399
|
+
try {
|
|
400
|
+
const data = JSON.parse(body);
|
|
401
|
+
const project = await db.updateProject(projectId, data);
|
|
402
|
+
if (!project) {
|
|
403
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
404
|
+
res.end(JSON.stringify({ error: 'Project not found' }));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
408
|
+
res.end(JSON.stringify(project));
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error('Project update error:', error);
|
|
411
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
412
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// DELETE /api/projects/:id
|
|
419
|
+
const projectDeleteMatch = url.match(/^\/api\/projects\/(\d+)$/);
|
|
420
|
+
if (projectDeleteMatch && method === 'DELETE') {
|
|
421
|
+
if (!auth.isAuthenticated(req)) {
|
|
422
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
423
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const projectId = parseInt(projectDeleteMatch[1]);
|
|
428
|
+
(async () => {
|
|
429
|
+
try {
|
|
430
|
+
const project = await db.deleteProject(projectId);
|
|
431
|
+
if (!project) {
|
|
432
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
433
|
+
res.end(JSON.stringify({ error: 'Project not found' }));
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
437
|
+
res.end(JSON.stringify({ success: true }));
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.error('Project delete error:', error);
|
|
440
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
441
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
442
|
+
}
|
|
443
|
+
})();
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// POST /api/agents/:id/project — assign agent to project
|
|
448
|
+
const agentProjectMatch = url.match(/^\/api\/agents\/([^/]+)\/project$/);
|
|
449
|
+
if (agentProjectMatch && method === 'POST') {
|
|
450
|
+
if (!auth.isAuthenticated(req)) {
|
|
451
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
452
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const agentId = agentProjectMatch[1];
|
|
457
|
+
let body = '';
|
|
458
|
+
req.on('data', chunk => body += chunk);
|
|
459
|
+
req.on('end', async () => {
|
|
460
|
+
try {
|
|
461
|
+
const { project_id } = JSON.parse(body);
|
|
462
|
+
const agent = await db.assignAgentToProject(agentId, project_id);
|
|
463
|
+
if (!agent) {
|
|
464
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
465
|
+
res.end(JSON.stringify({ error: 'Agent not found' }));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
469
|
+
res.end(JSON.stringify(agent));
|
|
470
|
+
} catch (error) {
|
|
471
|
+
console.error('Agent project assign error:', error);
|
|
472
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
473
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// GET /api/github/repos?org=<org>&token=<optional> — fetch repos from a GitHub org
|
|
480
|
+
if (url === '/api/github/repos' && method === 'GET') {
|
|
481
|
+
if (!auth.isAuthenticated(req)) {
|
|
482
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
483
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const urlObj = new URL(req.url, `http://${req.headers.host}`);
|
|
488
|
+
const org = urlObj.searchParams.get('org');
|
|
489
|
+
const projectToken = urlObj.searchParams.get('token') || null;
|
|
490
|
+
|
|
491
|
+
if (!org) {
|
|
492
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
493
|
+
res.end(JSON.stringify({ error: 'org parameter is required' }));
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
(async () => {
|
|
498
|
+
try {
|
|
499
|
+
// Resolve token: provided project token > unified OAuth table > legacy column > env default
|
|
500
|
+
let token = projectToken;
|
|
501
|
+
if (!token) {
|
|
502
|
+
const sessionId = auth.getSessionFromRequest(req);
|
|
503
|
+
const session = auth.getSession(sessionId);
|
|
504
|
+
if (session?.userId) {
|
|
505
|
+
const oauthProvider = await db.getOAuthProvider(session.userId, 'github');
|
|
506
|
+
if (oauthProvider?.access_token) {
|
|
507
|
+
token = encryption.decrypt(oauthProvider.access_token);
|
|
508
|
+
}
|
|
509
|
+
if (!token) {
|
|
510
|
+
const user = await db.getUserById(session.userId);
|
|
511
|
+
if (user?.github_token_encrypted) {
|
|
512
|
+
token = encryption.decrypt(user.github_token_encrypted);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (!token) {
|
|
518
|
+
token = process.env.GH_TOKEN || process.env.GITHUB_DEFAULT_TOKEN || null;
|
|
519
|
+
}
|
|
520
|
+
if (!token) {
|
|
521
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
522
|
+
res.end(JSON.stringify({ error: 'No GitHub token available' }));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Fetch repos from GitHub org (up to 100)
|
|
527
|
+
const ghRes = await fetch(`https://api.github.com/orgs/${encodeURIComponent(org)}/repos?per_page=100&sort=name`, {
|
|
528
|
+
headers: {
|
|
529
|
+
'Authorization': `Bearer ${token}`,
|
|
530
|
+
'Accept': 'application/vnd.github+json',
|
|
531
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
if (!ghRes.ok) {
|
|
536
|
+
const text = await ghRes.text();
|
|
537
|
+
res.writeHead(ghRes.status, { 'Content-Type': 'application/json' });
|
|
538
|
+
res.end(JSON.stringify({ error: `GitHub API error (${ghRes.status}): ${text}` }));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const repos = await ghRes.json();
|
|
543
|
+
const repoNames = repos.map(r => r.name).sort();
|
|
544
|
+
|
|
545
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
546
|
+
res.end(JSON.stringify(repoNames));
|
|
547
|
+
} catch (error) {
|
|
548
|
+
console.error('GitHub repos fetch error:', error);
|
|
549
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
550
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
551
|
+
}
|
|
552
|
+
})();
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// GET /api/github/project-fields?org=<org>&project_number=<num>&token=<optional>
|
|
557
|
+
// Fetch status field ID and options from a GitHub project board
|
|
558
|
+
if (url === '/api/github/project-fields' && method === 'GET') {
|
|
559
|
+
if (!auth.isAuthenticated(req)) {
|
|
560
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
561
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const urlObj = new URL(req.url, `http://${req.headers.host}`);
|
|
566
|
+
const org = urlObj.searchParams.get('org');
|
|
567
|
+
const projectNumber = parseInt(urlObj.searchParams.get('project_number'));
|
|
568
|
+
const projectToken = urlObj.searchParams.get('token') || null;
|
|
569
|
+
|
|
570
|
+
if (!org || !projectNumber) {
|
|
571
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
572
|
+
res.end(JSON.stringify({ error: 'org and project_number parameters are required' }));
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
(async () => {
|
|
577
|
+
try {
|
|
578
|
+
// Resolve token: unified OAuth table > legacy column > env default
|
|
579
|
+
let token = projectToken;
|
|
580
|
+
if (!token) {
|
|
581
|
+
const sessionId = auth.getSessionFromRequest(req);
|
|
582
|
+
const session = auth.getSession(sessionId);
|
|
583
|
+
if (session?.userId) {
|
|
584
|
+
const oauthProvider = await db.getOAuthProvider(session.userId, 'github');
|
|
585
|
+
if (oauthProvider?.access_token) {
|
|
586
|
+
token = encryption.decrypt(oauthProvider.access_token);
|
|
587
|
+
}
|
|
588
|
+
if (!token) {
|
|
589
|
+
const user = await db.getUserById(session.userId);
|
|
590
|
+
if (user?.github_token_encrypted) {
|
|
591
|
+
token = encryption.decrypt(user.github_token_encrypted);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (!token) {
|
|
597
|
+
token = process.env.GH_TOKEN || process.env.GITHUB_DEFAULT_TOKEN || null;
|
|
598
|
+
}
|
|
599
|
+
if (!token) {
|
|
600
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
601
|
+
res.end(JSON.stringify({ error: 'No GitHub token available' }));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// GraphQL query to fetch project ID, status field, and its options
|
|
606
|
+
const ghRes = await fetch('https://api.github.com/graphql', {
|
|
607
|
+
method: 'POST',
|
|
608
|
+
headers: {
|
|
609
|
+
'Authorization': `Bearer ${token}`,
|
|
610
|
+
'Content-Type': 'application/json'
|
|
611
|
+
},
|
|
612
|
+
body: JSON.stringify({
|
|
613
|
+
query: `query($org: String!, $num: Int!) {
|
|
614
|
+
organization(login: $org) {
|
|
615
|
+
projectV2(number: $num) {
|
|
616
|
+
id
|
|
617
|
+
title
|
|
618
|
+
fields(first: 30) {
|
|
619
|
+
nodes {
|
|
620
|
+
... on ProjectV2SingleSelectField {
|
|
621
|
+
id
|
|
622
|
+
name
|
|
623
|
+
options {
|
|
624
|
+
id
|
|
625
|
+
name
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}`,
|
|
633
|
+
variables: { org, num: projectNumber }
|
|
634
|
+
})
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
if (!ghRes.ok) {
|
|
638
|
+
const text = await ghRes.text();
|
|
639
|
+
res.writeHead(ghRes.status, { 'Content-Type': 'application/json' });
|
|
640
|
+
res.end(JSON.stringify({ error: `GitHub GraphQL error (${ghRes.status}): ${text}` }));
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const json = await ghRes.json();
|
|
645
|
+
if (json.errors) {
|
|
646
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
647
|
+
res.end(JSON.stringify({ error: json.errors[0].message }));
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const project = json.data.organization.projectV2;
|
|
652
|
+
// Find the Status field (single-select field named "Status")
|
|
653
|
+
const statusField = project.fields.nodes.find(f => f.name === 'Status' && f.options);
|
|
654
|
+
|
|
655
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
656
|
+
res.end(JSON.stringify({
|
|
657
|
+
project_id: project.id,
|
|
658
|
+
project_title: project.title,
|
|
659
|
+
status_field: statusField ? {
|
|
660
|
+
id: statusField.id,
|
|
661
|
+
name: statusField.name,
|
|
662
|
+
options: statusField.options
|
|
663
|
+
} : null
|
|
664
|
+
}));
|
|
665
|
+
} catch (error) {
|
|
666
|
+
console.error('GitHub project fields fetch error:', error);
|
|
667
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
668
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
669
|
+
}
|
|
670
|
+
})();
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Route not handled
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
module.exports = {
|
|
679
|
+
registerRoutes
|
|
680
|
+
};
|