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.
@@ -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
- // All admin routes require authentication and admin role
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
- // signature available at req.headers['x-hub-signature-256'] for verification
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 (if webhook secret is set)
471
- // Note: In production, you'd verify the signature properly using:
472
- // githubService.verifyWebhookSignature(JSON.stringify(req.body), signature, secret)
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