archicore 0.2.0 → 0.2.1
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/cli/commands/interactive.js +51 -19
- package/dist/github/github-service.d.ts +5 -1
- package/dist/github/github-service.js +21 -3
- package/dist/semantic-memory/embedding-service.d.ts +8 -1
- package/dist/semantic-memory/embedding-service.js +141 -47
- package/dist/server/index.js +66 -1
- package/dist/server/routes/admin.js +149 -1
- package/dist/server/routes/auth.js +46 -0
- package/dist/server/routes/github.js +17 -4
- package/dist/server/services/audit-service.d.ts +88 -0
- package/dist/server/services/audit-service.js +380 -0
- package/dist/server/services/auth-service.d.ts +11 -5
- package/dist/server/services/auth-service.js +299 -52
- package/dist/server/services/cache.d.ts +77 -0
- package/dist/server/services/cache.js +245 -0
- package/dist/server/services/database.d.ts +43 -0
- package/dist/server/services/database.js +221 -0
- package/package.json +17 -2
|
@@ -2,14 +2,33 @@
|
|
|
2
2
|
* Admin API Routes for ArchiCore
|
|
3
3
|
*/
|
|
4
4
|
import { Router } from 'express';
|
|
5
|
+
import rateLimit from 'express-rate-limit';
|
|
5
6
|
import { AuthService } from '../services/auth-service.js';
|
|
7
|
+
import { auditService } from '../services/audit-service.js';
|
|
6
8
|
import { authMiddleware, adminMiddleware } from './auth.js';
|
|
7
9
|
import { Logger } from '../../utils/logger.js';
|
|
8
10
|
export const adminRouter = Router();
|
|
9
11
|
const authService = AuthService.getInstance();
|
|
10
|
-
//
|
|
12
|
+
// Stricter rate limiting for admin routes (30 requests per minute)
|
|
13
|
+
const adminRateLimiter = rateLimit({
|
|
14
|
+
windowMs: 60 * 1000, // 1 minute
|
|
15
|
+
max: 30, // 30 requests per minute
|
|
16
|
+
message: { error: 'Too many admin requests, please slow down', retryAfter: 60 },
|
|
17
|
+
standardHeaders: true,
|
|
18
|
+
legacyHeaders: false,
|
|
19
|
+
handler: (_req, res) => {
|
|
20
|
+
Logger.warn('Admin rate limit exceeded');
|
|
21
|
+
res.status(429).json({
|
|
22
|
+
error: 'Too many admin requests',
|
|
23
|
+
message: 'Please wait before making more admin API calls',
|
|
24
|
+
retryAfter: 60
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
// All admin routes require authentication, admin role, and rate limiting
|
|
11
29
|
adminRouter.use(authMiddleware);
|
|
12
30
|
adminRouter.use(adminMiddleware);
|
|
31
|
+
adminRouter.use(adminRateLimiter);
|
|
13
32
|
/**
|
|
14
33
|
* GET /api/admin/users
|
|
15
34
|
* Get all users
|
|
@@ -49,6 +68,8 @@ adminRouter.get('/users/:id', async (req, res) => {
|
|
|
49
68
|
* Update user's subscription tier
|
|
50
69
|
*/
|
|
51
70
|
adminRouter.put('/users/:id/tier', async (req, res) => {
|
|
71
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
72
|
+
const userAgent = req.headers['user-agent'];
|
|
52
73
|
try {
|
|
53
74
|
const { id } = req.params;
|
|
54
75
|
const { tier } = req.body;
|
|
@@ -57,11 +78,28 @@ adminRouter.put('/users/:id/tier', async (req, res) => {
|
|
|
57
78
|
res.status(400).json({ error: 'Invalid tier. Valid tiers: ' + validTiers.join(', ') });
|
|
58
79
|
return;
|
|
59
80
|
}
|
|
81
|
+
// Get old tier for audit
|
|
82
|
+
const oldUser = await authService.getUser(id);
|
|
83
|
+
const oldTier = oldUser?.tier;
|
|
60
84
|
const updated = await authService.updateUserTier(id, tier);
|
|
61
85
|
if (!updated) {
|
|
62
86
|
res.status(404).json({ error: 'User not found' });
|
|
63
87
|
return;
|
|
64
88
|
}
|
|
89
|
+
// Audit log
|
|
90
|
+
await auditService.log({
|
|
91
|
+
userId: req.user?.id,
|
|
92
|
+
username: req.user?.username,
|
|
93
|
+
action: 'admin.tier_change',
|
|
94
|
+
ip,
|
|
95
|
+
userAgent,
|
|
96
|
+
details: {
|
|
97
|
+
targetUserId: id,
|
|
98
|
+
targetUsername: oldUser?.username,
|
|
99
|
+
oldTier,
|
|
100
|
+
newTier: tier
|
|
101
|
+
}
|
|
102
|
+
});
|
|
65
103
|
res.json({ success: true });
|
|
66
104
|
}
|
|
67
105
|
catch (error) {
|
|
@@ -74,13 +112,30 @@ adminRouter.put('/users/:id/tier', async (req, res) => {
|
|
|
74
112
|
* Delete user
|
|
75
113
|
*/
|
|
76
114
|
adminRouter.delete('/users/:id', async (req, res) => {
|
|
115
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
116
|
+
const userAgent = req.headers['user-agent'];
|
|
77
117
|
try {
|
|
78
118
|
const { id } = req.params;
|
|
119
|
+
// Get user info before deletion for audit
|
|
120
|
+
const userToDelete = await authService.getUser(id);
|
|
79
121
|
const deleted = await authService.deleteUser(id);
|
|
80
122
|
if (!deleted) {
|
|
81
123
|
res.status(400).json({ error: 'Cannot delete user (admin or not found)' });
|
|
82
124
|
return;
|
|
83
125
|
}
|
|
126
|
+
// Audit log
|
|
127
|
+
await auditService.log({
|
|
128
|
+
userId: req.user?.id,
|
|
129
|
+
username: req.user?.username,
|
|
130
|
+
action: 'admin.user_delete',
|
|
131
|
+
ip,
|
|
132
|
+
userAgent,
|
|
133
|
+
details: {
|
|
134
|
+
deletedUserId: id,
|
|
135
|
+
deletedUsername: userToDelete?.username,
|
|
136
|
+
deletedEmail: userToDelete?.email
|
|
137
|
+
}
|
|
138
|
+
});
|
|
84
139
|
res.json({ success: true });
|
|
85
140
|
}
|
|
86
141
|
catch (error) {
|
|
@@ -120,4 +175,97 @@ adminRouter.get('/stats', async (_req, res) => {
|
|
|
120
175
|
res.status(500).json({ error: 'Failed to get statistics' });
|
|
121
176
|
}
|
|
122
177
|
});
|
|
178
|
+
// ===== AUDIT LOGS =====
|
|
179
|
+
/**
|
|
180
|
+
* GET /api/admin/audit
|
|
181
|
+
* Get audit logs with filtering and pagination
|
|
182
|
+
*/
|
|
183
|
+
adminRouter.get('/audit', async (req, res) => {
|
|
184
|
+
try {
|
|
185
|
+
const { userId, action, severity, success, startDate, endDate, limit = '50', offset = '0' } = req.query;
|
|
186
|
+
// Log that admin is viewing audit logs
|
|
187
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
188
|
+
const userAgent = req.headers['user-agent'];
|
|
189
|
+
await auditService.log({
|
|
190
|
+
userId: req.user?.id,
|
|
191
|
+
username: req.user?.username,
|
|
192
|
+
action: 'admin.view_logs',
|
|
193
|
+
ip,
|
|
194
|
+
userAgent,
|
|
195
|
+
details: { filters: { userId, action, severity } }
|
|
196
|
+
});
|
|
197
|
+
const result = await auditService.query({
|
|
198
|
+
userId: userId,
|
|
199
|
+
action: action,
|
|
200
|
+
severity: severity,
|
|
201
|
+
success: success ? success === 'true' : undefined,
|
|
202
|
+
startDate: startDate ? new Date(startDate) : undefined,
|
|
203
|
+
endDate: endDate ? new Date(endDate) : undefined,
|
|
204
|
+
limit: parseInt(limit, 10),
|
|
205
|
+
offset: parseInt(offset, 10)
|
|
206
|
+
});
|
|
207
|
+
res.json(result);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
Logger.error('Failed to get audit logs:', error);
|
|
211
|
+
res.status(500).json({ error: 'Failed to get audit logs' });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
/**
|
|
215
|
+
* GET /api/admin/audit/stats
|
|
216
|
+
* Get audit statistics
|
|
217
|
+
*/
|
|
218
|
+
adminRouter.get('/audit/stats', async (req, res) => {
|
|
219
|
+
try {
|
|
220
|
+
const days = parseInt(req.query.days || '7', 10);
|
|
221
|
+
const stats = await auditService.getStats(days);
|
|
222
|
+
res.json(stats);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
Logger.error('Failed to get audit stats:', error);
|
|
226
|
+
res.status(500).json({ error: 'Failed to get audit statistics' });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
/**
|
|
230
|
+
* GET /api/admin/audit/user/:userId
|
|
231
|
+
* Get audit logs for specific user
|
|
232
|
+
*/
|
|
233
|
+
adminRouter.get('/audit/user/:userId', async (req, res) => {
|
|
234
|
+
try {
|
|
235
|
+
const { userId } = req.params;
|
|
236
|
+
const limit = parseInt(req.query.limit || '50', 10);
|
|
237
|
+
const logs = await auditService.getUserLogs(userId, limit);
|
|
238
|
+
res.json({ logs });
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
Logger.error('Failed to get user audit logs:', error);
|
|
242
|
+
res.status(500).json({ error: 'Failed to get user audit logs' });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
/**
|
|
246
|
+
* POST /api/admin/audit/cleanup
|
|
247
|
+
* Clean up old audit logs (retention policy)
|
|
248
|
+
*/
|
|
249
|
+
adminRouter.post('/audit/cleanup', async (req, res) => {
|
|
250
|
+
try {
|
|
251
|
+
const retentionDays = parseInt(req.body.retentionDays || '90', 10);
|
|
252
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
253
|
+
const userAgent = req.headers['user-agent'];
|
|
254
|
+
// Log cleanup action
|
|
255
|
+
await auditService.log({
|
|
256
|
+
userId: req.user?.id,
|
|
257
|
+
username: req.user?.username,
|
|
258
|
+
action: 'admin.view_logs',
|
|
259
|
+
ip,
|
|
260
|
+
userAgent,
|
|
261
|
+
details: { operation: 'cleanup', retentionDays }
|
|
262
|
+
});
|
|
263
|
+
const removed = await auditService.cleanup(retentionDays);
|
|
264
|
+
res.json({ success: true, removed });
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
Logger.error('Failed to cleanup audit logs:', error);
|
|
268
|
+
res.status(500).json({ error: 'Failed to cleanup audit logs' });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
123
271
|
//# sourceMappingURL=admin.js.map
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Router } from 'express';
|
|
5
5
|
import { AuthService } from '../services/auth-service.js';
|
|
6
|
+
import { auditService } from '../services/audit-service.js';
|
|
6
7
|
import { Logger } from '../../utils/logger.js';
|
|
7
8
|
export const authRouter = Router();
|
|
8
9
|
const authService = AuthService.getInstance();
|
|
@@ -35,6 +36,8 @@ export async function adminMiddleware(req, res, next) {
|
|
|
35
36
|
* Register new user
|
|
36
37
|
*/
|
|
37
38
|
authRouter.post('/register', async (req, res) => {
|
|
39
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
40
|
+
const userAgent = req.headers['user-agent'];
|
|
38
41
|
try {
|
|
39
42
|
const { email, username, password } = req.body;
|
|
40
43
|
if (!email || !username || !password) {
|
|
@@ -46,6 +49,17 @@ authRouter.post('/register', async (req, res) => {
|
|
|
46
49
|
return;
|
|
47
50
|
}
|
|
48
51
|
const result = await authService.register(email, username, password);
|
|
52
|
+
// Audit log
|
|
53
|
+
if (result.success && result.user) {
|
|
54
|
+
await auditService.log({
|
|
55
|
+
userId: result.user.id,
|
|
56
|
+
username: result.user.username,
|
|
57
|
+
action: 'auth.register',
|
|
58
|
+
ip,
|
|
59
|
+
userAgent,
|
|
60
|
+
details: { email }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
49
63
|
res.json(result);
|
|
50
64
|
}
|
|
51
65
|
catch (error) {
|
|
@@ -58,6 +72,8 @@ authRouter.post('/register', async (req, res) => {
|
|
|
58
72
|
* Login with email/password
|
|
59
73
|
*/
|
|
60
74
|
authRouter.post('/login', async (req, res) => {
|
|
75
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
76
|
+
const userAgent = req.headers['user-agent'];
|
|
61
77
|
try {
|
|
62
78
|
const { email, password } = req.body;
|
|
63
79
|
if (!email || !password) {
|
|
@@ -65,10 +81,30 @@ authRouter.post('/login', async (req, res) => {
|
|
|
65
81
|
return;
|
|
66
82
|
}
|
|
67
83
|
const result = await authService.login(email, password);
|
|
84
|
+
// Audit log
|
|
85
|
+
await auditService.log({
|
|
86
|
+
userId: result.user?.id,
|
|
87
|
+
username: result.user?.username,
|
|
88
|
+
action: 'auth.login',
|
|
89
|
+
ip,
|
|
90
|
+
userAgent,
|
|
91
|
+
details: { email },
|
|
92
|
+
success: result.success,
|
|
93
|
+
errorMessage: result.success ? undefined : result.error
|
|
94
|
+
});
|
|
68
95
|
res.json(result);
|
|
69
96
|
}
|
|
70
97
|
catch (error) {
|
|
71
98
|
Logger.error('Login error:', error);
|
|
99
|
+
// Audit failed login attempt
|
|
100
|
+
await auditService.log({
|
|
101
|
+
action: 'auth.login',
|
|
102
|
+
ip,
|
|
103
|
+
userAgent,
|
|
104
|
+
details: { email: req.body.email },
|
|
105
|
+
success: false,
|
|
106
|
+
errorMessage: 'Login failed'
|
|
107
|
+
});
|
|
72
108
|
res.status(500).json({ success: false, error: 'Login failed' });
|
|
73
109
|
}
|
|
74
110
|
});
|
|
@@ -77,11 +113,21 @@ authRouter.post('/login', async (req, res) => {
|
|
|
77
113
|
* Logout current user
|
|
78
114
|
*/
|
|
79
115
|
authRouter.post('/logout', authMiddleware, async (req, res) => {
|
|
116
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
117
|
+
const userAgent = req.headers['user-agent'];
|
|
80
118
|
try {
|
|
81
119
|
const token = req.headers.authorization?.substring(7);
|
|
82
120
|
if (token) {
|
|
83
121
|
await authService.logout(token);
|
|
84
122
|
}
|
|
123
|
+
// Audit log
|
|
124
|
+
await auditService.log({
|
|
125
|
+
userId: req.user?.id,
|
|
126
|
+
username: req.user?.username,
|
|
127
|
+
action: 'auth.logout',
|
|
128
|
+
ip,
|
|
129
|
+
userAgent
|
|
130
|
+
});
|
|
85
131
|
res.json({ success: true });
|
|
86
132
|
}
|
|
87
133
|
catch (error) {
|
|
@@ -449,7 +449,7 @@ githubRouter.post('/repositories/:id/analyze', authMiddleware, async (req, res)
|
|
|
449
449
|
githubRouter.post('/webhook', async (req, res) => {
|
|
450
450
|
try {
|
|
451
451
|
const event = req.headers['x-github-event'];
|
|
452
|
-
|
|
452
|
+
const signature = req.headers['x-hub-signature-256'];
|
|
453
453
|
const payload = req.body;
|
|
454
454
|
if (!event || !payload) {
|
|
455
455
|
res.status(400).json({ error: 'Invalid webhook' });
|
|
@@ -467,9 +467,22 @@ githubRouter.post('/webhook', async (req, res) => {
|
|
|
467
467
|
res.status(200).json({ message: 'Repository not connected' });
|
|
468
468
|
return;
|
|
469
469
|
}
|
|
470
|
-
// Verify signature (
|
|
471
|
-
|
|
472
|
-
|
|
470
|
+
// Verify webhook signature (HMAC-SHA256)
|
|
471
|
+
if (repo.webhookSecret) {
|
|
472
|
+
if (!signature) {
|
|
473
|
+
Logger.warn(`Webhook missing signature for ${fullName}`);
|
|
474
|
+
res.status(401).json({ error: 'Missing signature' });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const secret = githubService.getWebhookSecret(repo.webhookSecret);
|
|
478
|
+
const payloadBody = JSON.stringify(payload);
|
|
479
|
+
if (!githubService.verifyWebhookSignature(payloadBody, signature, secret)) {
|
|
480
|
+
Logger.warn(`Invalid webhook signature for ${fullName}`);
|
|
481
|
+
res.status(401).json({ error: 'Invalid signature' });
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
Logger.debug(`Webhook signature verified for ${fullName}`);
|
|
485
|
+
}
|
|
473
486
|
Logger.info(`Webhook received: ${event} for ${fullName}`);
|
|
474
487
|
// Handle different events
|
|
475
488
|
switch (event) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Log Service for ArchiCore
|
|
3
|
+
*
|
|
4
|
+
* Logs user actions for security and compliance purposes
|
|
5
|
+
* Supports PostgreSQL (primary) with JSON file fallback
|
|
6
|
+
*/
|
|
7
|
+
export type AuditAction = 'auth.login' | 'auth.logout' | 'auth.register' | 'auth.password_change' | 'auth.device_login' | 'user.update' | 'user.tier_change' | 'user.delete' | 'project.create' | 'project.delete' | 'project.analyze' | 'project.simulate' | 'project.ask' | 'project.docs' | 'github.connect' | 'github.disconnect' | 'github.repo_connect' | 'github.repo_disconnect' | 'github.webhook_received' | 'api.key_create' | 'api.key_revoke' | 'api.request' | 'admin.login' | 'admin.user_update' | 'admin.user_delete' | 'admin.tier_change' | 'admin.view_logs';
|
|
8
|
+
export type AuditSeverity = 'info' | 'warning' | 'critical';
|
|
9
|
+
export interface AuditLogEntry {
|
|
10
|
+
id: string;
|
|
11
|
+
timestamp: Date;
|
|
12
|
+
userId?: string;
|
|
13
|
+
username?: string;
|
|
14
|
+
action: AuditAction;
|
|
15
|
+
severity: AuditSeverity;
|
|
16
|
+
ip?: string;
|
|
17
|
+
userAgent?: string;
|
|
18
|
+
details?: Record<string, any>;
|
|
19
|
+
success: boolean;
|
|
20
|
+
errorMessage?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface AuditLogQuery {
|
|
23
|
+
userId?: string;
|
|
24
|
+
action?: AuditAction;
|
|
25
|
+
severity?: AuditSeverity;
|
|
26
|
+
startDate?: Date;
|
|
27
|
+
endDate?: Date;
|
|
28
|
+
success?: boolean;
|
|
29
|
+
limit?: number;
|
|
30
|
+
offset?: number;
|
|
31
|
+
}
|
|
32
|
+
export declare class AuditService {
|
|
33
|
+
private dataDir;
|
|
34
|
+
private logs;
|
|
35
|
+
private jsonInitialized;
|
|
36
|
+
private saveTimeout;
|
|
37
|
+
constructor(dataDir?: string);
|
|
38
|
+
private useDatabase;
|
|
39
|
+
private ensureJsonInitialized;
|
|
40
|
+
private saveJson;
|
|
41
|
+
private generateId;
|
|
42
|
+
private getSeverity;
|
|
43
|
+
private rowToEntry;
|
|
44
|
+
/**
|
|
45
|
+
* Log an action
|
|
46
|
+
*/
|
|
47
|
+
log(params: {
|
|
48
|
+
userId?: string;
|
|
49
|
+
username?: string;
|
|
50
|
+
action: AuditAction;
|
|
51
|
+
ip?: string;
|
|
52
|
+
userAgent?: string;
|
|
53
|
+
details?: Record<string, any>;
|
|
54
|
+
success?: boolean;
|
|
55
|
+
errorMessage?: string;
|
|
56
|
+
}): Promise<AuditLogEntry>;
|
|
57
|
+
/**
|
|
58
|
+
* Query audit logs
|
|
59
|
+
*/
|
|
60
|
+
query(params?: AuditLogQuery): Promise<{
|
|
61
|
+
logs: AuditLogEntry[];
|
|
62
|
+
total: number;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Get recent logs for a user
|
|
66
|
+
*/
|
|
67
|
+
getUserLogs(userId: string, limit?: number): Promise<AuditLogEntry[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Get failed login attempts for an IP
|
|
70
|
+
*/
|
|
71
|
+
getFailedLogins(ip: string, since: Date): Promise<number>;
|
|
72
|
+
/**
|
|
73
|
+
* Get statistics
|
|
74
|
+
*/
|
|
75
|
+
getStats(days?: number): Promise<{
|
|
76
|
+
totalActions: number;
|
|
77
|
+
uniqueUsers: number;
|
|
78
|
+
failedLogins: number;
|
|
79
|
+
criticalEvents: number;
|
|
80
|
+
actionCounts: Record<string, number>;
|
|
81
|
+
}>;
|
|
82
|
+
/**
|
|
83
|
+
* Clear old logs (retention policy)
|
|
84
|
+
*/
|
|
85
|
+
cleanup(retentionDays?: number): Promise<number>;
|
|
86
|
+
}
|
|
87
|
+
export declare const auditService: AuditService;
|
|
88
|
+
//# sourceMappingURL=audit-service.d.ts.map
|