archicore 0.1.9 → 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.
@@ -5,7 +5,9 @@ export class EmbeddingService {
5
5
  config;
6
6
  initialized = false;
7
7
  _isAvailable = false;
8
- jinaApiKey;
8
+ jinaApiKeys = [];
9
+ currentKeyIndex = 0;
10
+ keyFailures = new Map();
9
11
  embeddingDimension = 1024; // Jina default, OpenAI small = 1536
10
12
  constructor(config) {
11
13
  this.config = config;
@@ -23,16 +25,21 @@ export class EmbeddingService {
23
25
  }
24
26
  try {
25
27
  if (this.config.provider === 'jina') {
26
- // Jina AI - free embeddings
27
- this.jinaApiKey = process.env.JINA_API_KEY;
28
- if (!this.jinaApiKey) {
29
- Logger.warn('JINA_API_KEY not set - semantic search disabled');
28
+ // Jina AI - free embeddings with key rotation support
29
+ // Support multiple keys: JINA_API_KEYS (comma-separated) or JINA_API_KEY (single)
30
+ const keysEnv = process.env.JINA_API_KEYS || process.env.JINA_API_KEY || '';
31
+ this.jinaApiKeys = keysEnv
32
+ .split(',')
33
+ .map(k => k.trim())
34
+ .filter(k => k.length > 0);
35
+ if (this.jinaApiKeys.length === 0) {
36
+ Logger.warn('JINA_API_KEY(S) not set - semantic search disabled');
30
37
  Logger.info('Get free API key at: https://jina.ai/embeddings/');
31
38
  return;
32
39
  }
33
40
  this.embeddingDimension = 1024;
34
41
  this._isAvailable = true;
35
- Logger.success('Jina AI embeddings enabled');
42
+ Logger.success(`Jina AI embeddings enabled (${this.jinaApiKeys.length} key${this.jinaApiKeys.length > 1 ? 's' : ''} configured)`);
36
43
  }
37
44
  else {
38
45
  // OpenAI embeddings
@@ -51,6 +58,45 @@ export class EmbeddingService {
51
58
  Logger.warn('Embedding init failed: ' + error);
52
59
  }
53
60
  }
61
+ getCurrentJinaKey() {
62
+ if (this.jinaApiKeys.length === 0)
63
+ return undefined;
64
+ return this.jinaApiKeys[this.currentKeyIndex];
65
+ }
66
+ rotateToNextKey() {
67
+ if (this.jinaApiKeys.length <= 1)
68
+ return false;
69
+ const oldIndex = this.currentKeyIndex;
70
+ this.currentKeyIndex = (this.currentKeyIndex + 1) % this.jinaApiKeys.length;
71
+ // Track failure for old key
72
+ const failure = this.keyFailures.get(oldIndex) || { count: 0, lastFail: new Date() };
73
+ failure.count++;
74
+ failure.lastFail = new Date();
75
+ this.keyFailures.set(oldIndex, failure);
76
+ Logger.warn(`Jina API key ${oldIndex + 1} exhausted/failed, rotating to key ${this.currentKeyIndex + 1}`);
77
+ return true;
78
+ }
79
+ shouldSkipKey(index) {
80
+ const failure = this.keyFailures.get(index);
81
+ if (!failure)
82
+ return false;
83
+ // Skip key if it failed recently (within 5 minutes) and has many failures
84
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
85
+ return failure.count >= 3 && failure.lastFail > fiveMinutesAgo;
86
+ }
87
+ findWorkingKeyIndex() {
88
+ const startIndex = this.currentKeyIndex;
89
+ for (let i = 0; i < this.jinaApiKeys.length; i++) {
90
+ const index = (startIndex + i) % this.jinaApiKeys.length;
91
+ if (!this.shouldSkipKey(index)) {
92
+ return index;
93
+ }
94
+ }
95
+ // All keys are marked as failed, reset failures and try first key
96
+ Logger.warn('All Jina API keys marked as failed, resetting failure counters');
97
+ this.keyFailures.clear();
98
+ return 0;
99
+ }
54
100
  isAvailable() {
55
101
  this.ensureInitialized();
56
102
  return this._isAvailable;
@@ -60,7 +106,7 @@ export class EmbeddingService {
60
106
  if (!this._isAvailable)
61
107
  return new Array(this.embeddingDimension).fill(0);
62
108
  try {
63
- if (this.config.provider === 'jina' && this.jinaApiKey) {
109
+ if (this.config.provider === 'jina' && this.jinaApiKeys.length > 0) {
64
110
  return await this.generateJinaEmbedding(text);
65
111
  }
66
112
  else if (this.openai) {
@@ -73,28 +119,51 @@ export class EmbeddingService {
73
119
  return new Array(this.embeddingDimension).fill(0);
74
120
  }
75
121
  }
76
- async generateJinaEmbedding(text) {
77
- if (!this.jinaApiKey)
122
+ async generateJinaEmbedding(text, retryCount = 0) {
123
+ const maxRetries = this.jinaApiKeys.length;
124
+ // Find a working key index before first attempt
125
+ if (retryCount === 0) {
126
+ this.currentKeyIndex = this.findWorkingKeyIndex();
127
+ }
128
+ const apiKey = this.getCurrentJinaKey();
129
+ if (!apiKey)
78
130
  throw new Error('Jina API key not set');
79
- const response = await fetch('https://api.jina.ai/v1/embeddings', {
80
- method: 'POST',
81
- headers: {
82
- 'Content-Type': 'application/json',
83
- 'Authorization': `Bearer ${this.jinaApiKey}`
84
- },
85
- body: JSON.stringify({
86
- model: this.config.model || 'jina-embeddings-v3',
87
- task: 'text-matching',
88
- dimensions: 1024,
89
- input: [text.substring(0, 8000)]
90
- })
91
- });
92
- if (!response.ok) {
93
- const error = await response.text();
94
- throw new Error(`Jina API error: ${response.status} ${error}`);
131
+ try {
132
+ const response = await fetch('https://api.jina.ai/v1/embeddings', {
133
+ method: 'POST',
134
+ headers: {
135
+ 'Content-Type': 'application/json',
136
+ 'Authorization': `Bearer ${apiKey}`
137
+ },
138
+ body: JSON.stringify({
139
+ model: this.config.model || 'jina-embeddings-v3',
140
+ task: 'text-matching',
141
+ dimensions: 1024,
142
+ input: [text.substring(0, 8000)]
143
+ })
144
+ });
145
+ if (!response.ok) {
146
+ const errorText = await response.text();
147
+ // Check for rate limit or auth errors - try next key
148
+ if (response.status === 429 || response.status === 401 || response.status === 403) {
149
+ if (retryCount < maxRetries - 1 && this.rotateToNextKey()) {
150
+ Logger.warn(`Jina API key error (${response.status}), trying next key...`);
151
+ return this.generateJinaEmbedding(text, retryCount + 1);
152
+ }
153
+ }
154
+ throw new Error(`Jina API error: ${response.status} ${errorText}`);
155
+ }
156
+ const data = await response.json();
157
+ return data.data[0].embedding;
158
+ }
159
+ catch (error) {
160
+ // Network error or other issue - try next key
161
+ if (retryCount < maxRetries - 1 && this.rotateToNextKey()) {
162
+ Logger.warn(`Jina API request failed, trying next key...`);
163
+ return this.generateJinaEmbedding(text, retryCount + 1);
164
+ }
165
+ throw error;
95
166
  }
96
- const data = await response.json();
97
- return data.data[0].embedding;
98
167
  }
99
168
  /**
100
169
  * Generate embeddings for multiple texts - uses true batch API when available
@@ -110,30 +179,13 @@ export class EmbeddingService {
110
179
  Logger.progress(`Generating embeddings for ${texts.length} texts...`);
111
180
  try {
112
181
  // Use true batch API for Jina (much faster!)
113
- if (this.config.provider === 'jina' && this.jinaApiKey) {
182
+ if (this.config.provider === 'jina' && this.jinaApiKeys.length > 0) {
114
183
  const BATCH_SIZE = 500;
115
184
  const allEmbeddings = [];
116
185
  for (let i = 0; i < texts.length; i += BATCH_SIZE) {
117
186
  const batch = texts.slice(i, i + BATCH_SIZE).map(t => t.substring(0, 8000));
118
- const response = await fetch('https://api.jina.ai/v1/embeddings', {
119
- method: 'POST',
120
- headers: {
121
- 'Content-Type': 'application/json',
122
- 'Authorization': `Bearer ${this.jinaApiKey}`
123
- },
124
- body: JSON.stringify({
125
- model: this.config.model || 'jina-embeddings-v3',
126
- task: 'text-matching',
127
- dimensions: 1024,
128
- input: batch
129
- })
130
- });
131
- if (!response.ok) {
132
- const error = await response.text();
133
- throw new Error(`Jina API error: ${response.status} ${error}`);
134
- }
135
- const data = await response.json();
136
- allEmbeddings.push(...data.data.map(d => d.embedding));
187
+ const batchEmbeddings = await this.generateJinaBatchWithRetry(batch);
188
+ allEmbeddings.push(...batchEmbeddings);
137
189
  if (progressCallback) {
138
190
  progressCallback(Math.min(i + BATCH_SIZE, texts.length), texts.length);
139
191
  }
@@ -161,6 +213,48 @@ export class EmbeddingService {
161
213
  return texts.map(() => new Array(this.embeddingDimension).fill(0));
162
214
  }
163
215
  }
216
+ async generateJinaBatchWithRetry(batch, retryCount = 0) {
217
+ const maxRetries = this.jinaApiKeys.length;
218
+ const apiKey = this.getCurrentJinaKey();
219
+ if (!apiKey)
220
+ throw new Error('Jina API key not set');
221
+ try {
222
+ const response = await fetch('https://api.jina.ai/v1/embeddings', {
223
+ method: 'POST',
224
+ headers: {
225
+ 'Content-Type': 'application/json',
226
+ 'Authorization': `Bearer ${apiKey}`
227
+ },
228
+ body: JSON.stringify({
229
+ model: this.config.model || 'jina-embeddings-v3',
230
+ task: 'text-matching',
231
+ dimensions: 1024,
232
+ input: batch
233
+ })
234
+ });
235
+ if (!response.ok) {
236
+ const errorText = await response.text();
237
+ // Check for rate limit or auth errors - try next key
238
+ if (response.status === 429 || response.status === 401 || response.status === 403) {
239
+ if (retryCount < maxRetries - 1 && this.rotateToNextKey()) {
240
+ Logger.warn(`Jina API batch error (${response.status}), trying next key...`);
241
+ return this.generateJinaBatchWithRetry(batch, retryCount + 1);
242
+ }
243
+ }
244
+ throw new Error(`Jina API error: ${response.status} ${errorText}`);
245
+ }
246
+ const data = await response.json();
247
+ return data.data.map(d => d.embedding);
248
+ }
249
+ catch (error) {
250
+ // Network error or other issue - try next key
251
+ if (retryCount < maxRetries - 1 && this.rotateToNextKey()) {
252
+ Logger.warn(`Jina API batch request failed, trying next key...`);
253
+ return this.generateJinaBatchWithRetry(batch, retryCount + 1);
254
+ }
255
+ throw error;
256
+ }
257
+ }
164
258
  async generateOpenAIEmbedding(text) {
165
259
  if (!this.openai)
166
260
  throw new Error('OpenAI client not initialized');
@@ -25,6 +25,9 @@ import { adminRouter } from './routes/admin.js';
25
25
  import { developerRouter } from './routes/developer.js';
26
26
  import { githubRouter } from './routes/github.js';
27
27
  import deviceAuthRouter from './routes/device-auth.js';
28
+ import { cache } from './services/cache.js';
29
+ import { db } from './services/database.js';
30
+ import { AuthService } from './services/auth-service.js';
28
31
  const __filename = fileURLToPath(import.meta.url);
29
32
  const __dirname = path.dirname(__filename);
30
33
  // CORS whitelist - настройте под свои домены
@@ -156,6 +159,7 @@ export class ArchiCoreServer {
156
159
  this.app.use(express.static(publicPath, {
157
160
  maxAge: process.env.NODE_ENV === 'production' ? '1d' : 0,
158
161
  etag: true,
162
+ index: false, // Отключаем автоматический index.html - используем явные маршруты
159
163
  }));
160
164
  }
161
165
  setupRoutes() {
@@ -187,7 +191,54 @@ export class ArchiCoreServer {
187
191
  const deviceAuthPath = path.join(__dirname, '../../public/device-auth.html');
188
192
  res.sendFile(deviceAuthPath);
189
193
  });
190
- // SPA fallback - все остальные маршруты отдают index.html
194
+ // Clean URL routing
195
+ // Landing page (главная)
196
+ this.app.get('/', (_req, res) => {
197
+ res.sendFile(path.join(__dirname, '../../public/landing.html'));
198
+ });
199
+ // Dashboard (после авторизации)
200
+ this.app.get('/dashboard', (_req, res) => {
201
+ res.sendFile(path.join(__dirname, '../../public/index.html'));
202
+ });
203
+ // Add new project page
204
+ this.app.get('/new', (_req, res) => {
205
+ res.sendFile(path.join(__dirname, '../../public/index.html'));
206
+ });
207
+ // Pricing page
208
+ this.app.get('/pricing', (_req, res) => {
209
+ res.sendFile(path.join(__dirname, '../../public/pricing.html'));
210
+ });
211
+ // API Dashboard page
212
+ this.app.get('/api-dashboard', (_req, res) => {
213
+ res.sendFile(path.join(__dirname, '../../public/api-dashboard.html'));
214
+ });
215
+ // Auth page
216
+ this.app.get('/auth', (_req, res) => {
217
+ res.sendFile(path.join(__dirname, '../../public/auth.html'));
218
+ });
219
+ // Login alias
220
+ this.app.get('/login', (_req, res) => {
221
+ res.sendFile(path.join(__dirname, '../../public/auth.html'));
222
+ });
223
+ // Register alias
224
+ this.app.get('/register', (_req, res) => {
225
+ res.redirect('/auth?mode=register');
226
+ });
227
+ // Admin page
228
+ this.app.get('/admin', (_req, res) => {
229
+ res.sendFile(path.join(__dirname, '../../public/admin.html'));
230
+ });
231
+ // Legal pages
232
+ this.app.get('/privacy', (_req, res) => {
233
+ res.sendFile(path.join(__dirname, '../../public/privacy.html'));
234
+ });
235
+ this.app.get('/terms', (_req, res) => {
236
+ res.sendFile(path.join(__dirname, '../../public/terms.html'));
237
+ });
238
+ this.app.get('/security', (_req, res) => {
239
+ res.sendFile(path.join(__dirname, '../../public/security.html'));
240
+ });
241
+ // SPA fallback - все остальные маршруты отдают index.html (dashboard)
191
242
  this.app.get('/*splat', (_req, res) => {
192
243
  const indexPath = path.join(__dirname, '../../public/index.html');
193
244
  res.sendFile(indexPath);
@@ -208,6 +259,17 @@ export class ArchiCoreServer {
208
259
  });
209
260
  }
210
261
  async start() {
262
+ // Initialize database
263
+ try {
264
+ await db.init();
265
+ const authService = AuthService.getInstance();
266
+ await authService.initDatabase();
267
+ }
268
+ catch (error) {
269
+ Logger.warn('Database initialization failed, using JSON fallback:', error);
270
+ }
271
+ // Initialize cache
272
+ await cache.connect();
211
273
  return new Promise((resolve) => {
212
274
  this.server = createServer(this.app);
213
275
  const host = this.config.host || '0.0.0.0';
@@ -221,6 +283,9 @@ export class ArchiCoreServer {
221
283
  });
222
284
  }
223
285
  async stop() {
286
+ // Disconnect cache and database
287
+ await cache.disconnect();
288
+ await db.close();
224
289
  return new Promise((resolve) => {
225
290
  if (this.server) {
226
291
  this.server.close(() => {
@@ -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) {