Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion server/src/db/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Database from 'better-sqlite3';
import fs from 'node:fs';
import path from 'node:path';
import { config } from '../config.js';
import { logger } from '../services/logger.js';

let db: Database.Database | null = null;

Expand All @@ -24,7 +25,7 @@ export function getDb(): Database.Database {

return db;
} catch (error) {
console.error('Failed to initialize database:', error);
logger.errorFromError('db.connection', 'Failed to initialize database', error);
throw new Error('Database initialization failed');
}
}
Expand Down
5 changes: 3 additions & 2 deletions server/src/db/migrations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type Database from 'better-sqlite3';
import { initializeSchema } from './schema.js';
import { logger } from '../services/logger.js';

const migrations: Record<number, (db: Database.Database) => void> = {
1: (db) => {
Expand Down Expand Up @@ -31,10 +32,10 @@ export function runMigrations(db: Database.Database): void {
for (let v = currentVersion + 1; v <= targetVersion; v++) {
const migration = migrations[v];
if (migration) {
console.log(`Applying migration v${v}...`);
logger.info('db.migration', `Applying migration v${v}...`);
migration(db);
db.prepare('INSERT OR REPLACE INTO schema_version (version) VALUES (?)').run(v);
console.log(`Migration v${v} applied.`);
logger.info('db.migration', `Migration v${v} applied.`);
}
}
});
Expand Down
17 changes: 11 additions & 6 deletions server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import morgan from 'morgan';
import { config } from './config.js';
import { authMiddleware } from './middleware/auth.js';
import { errorHandler } from './middleware/errorHandler.js';
import { logger, morganLoggerStream } from './services/logger.js';
import { getDb, closeDb } from './db/connection.js';
import { runMigrations } from './db/migrations.js';
import healthRouter from './routes/health.js';
Expand All @@ -14,14 +15,15 @@ import categoriesRouter from './routes/categories.js';
import configsRouter from './routes/configs.js';
import syncRouter from './routes/sync.js';
import proxyRouter from './routes/proxy.js';
import logsRouter from './routes/logs.js';

export function createApp(): express.Express {
const app = express();

// Middleware
app.use(helmet());
app.use(cors());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
app.use(morgan('combined'));
app.use(morgan('combined', { stream: morganLoggerStream }));
app.use(express.json({ limit: '50mb' }));

// Auth middleware for all /api/* except /api/health
Expand All @@ -40,6 +42,9 @@ export function createApp(): express.Express {
// Wave 3: Proxy routes
app.use(proxyRouter);

// Wave 4: Logs route
app.use(logsRouter);

// Global error handler
app.use(errorHandler);

Expand All @@ -50,23 +55,23 @@ function startServer(): void {
// Initialize database
const db = getDb();
runMigrations(db);
console.log('✅ Database initialized');
logger.info('server.init', 'Database initialized');

const app = createApp();

const server = app.listen(config.port, () => {
console.log(`🚀 Server running on port ${config.port}`);
logger.info('server.start', `Server running on port ${config.port}`);
if (!config.apiSecret) {
console.warn('⚠️ Running without API_SECRET — auth is disabled');
logger.warn('server.auth', 'Running without API_SECRET — auth is disabled');
}
});

// Graceful shutdown
const shutdown = () => {
console.log('\n🛑 Shutting down...');
logger.info('server.shutdown', 'Shutting down...');
server.close(() => {
closeDb();
console.log('👋 Server stopped');
logger.info('server.shutdown', 'Server stopped');
process.exit(0);
});
};
Expand Down
3 changes: 2 additions & 1 deletion server/src/middleware/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { Request, Response, NextFunction } from 'express';
import { logger } from '../services/logger.js';

export function errorHandler(
err: Error,
_req: Request,
res: Response,
_next: NextFunction
): void {
console.error('Unhandled error:', err.stack || err.message);
logger.errorFromError('errorHandler.global', 'Unhandled error', err);

if (res.headersSent) {
return _next(err);
Expand Down
35 changes: 18 additions & 17 deletions server/src/routes/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Router } from 'express';
import { getDb } from '../db/connection.js';
import { encrypt, decrypt } from '../services/crypto.js';
import { config } from '../config.js';
import { logger } from '../services/logger.js';

const router = Router();

Expand Down Expand Up @@ -29,7 +30,7 @@ function getMaskedSecretResult(params: {
const detail = [configId ? `id=${String(configId)}` : '', configName ? `name=${String(configName)}` : '']
.filter(Boolean)
.join(', ');
console.warn(`[configs] Failed to decrypt ${kind}${detail ? ` (${detail})` : ''}:`, error);
logger.warn('configs.decrypt', `Failed to decrypt ${kind}${detail ? ` (${detail})` : ''}`, { kind, detail });
return { decryptedValue: '', status: 'decrypt_failed' };
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}
Expand Down Expand Up @@ -73,7 +74,7 @@ router.get('/api/configs/ai', (req, res) => {
});
res.json(configs);
} catch (err) {
console.error('GET /api/configs/ai error:', err);
logger.errorFromError('configs.getAI', 'GET /api/configs/ai error', err);
res.status(500).json({ error: 'Failed to fetch AI configs', code: 'FETCH_AI_CONFIGS_FAILED' });
}
});
Expand All @@ -95,7 +96,7 @@ router.post('/api/configs/ai', (req, res) => {

res.status(201).json({ id: result.lastInsertRowid, name, apiType, model, baseUrl, apiKey: maskApiKey(apiKey as string), isActive: !!isActive, reasoningEffort: reasoningEffort ?? null });
} catch (err) {
console.error('POST /api/configs/ai error:', err);
logger.errorFromError('configs.createAI', 'POST /api/configs/ai error', err);
res.status(500).json({ error: 'Failed to create AI config', code: 'CREATE_AI_CONFIG_FAILED' });
}
});
Expand Down Expand Up @@ -147,7 +148,7 @@ router.put('/api/configs/ai/bulk', (req, res) => {
try {
encryptedKey = encrypt(String(c.apiKey), config.encryptionKey);
} catch (encErr) {
console.error(`[configs] Failed to encrypt API key for config "${c.name}" (${c.id}), falling back to existing key:`, encErr);
logger.errorFromError('configs.encryptAIKey', `Failed to encrypt API key for config "${c.name}" (${c.id})`, encErr);
encryptedKey = existingKeys.get(String(c.id)) ?? '';
if (!encryptedKey) {
syncResult.skipped.push({ id: c.id, name: c.name ?? '', reason: 'encrypt_failed' });
Expand Down Expand Up @@ -178,7 +179,7 @@ router.put('/api/configs/ai/bulk', (req, res) => {
}

if (syncResult.skipped.length > 0) {
console.warn('[configs] Skipped AI configs with missing keys:', syncResult.skipped);
logger.warn('configs.bulkAI', 'Skipped AI configs with missing keys', { skippedCount: syncResult.skipped.length, skipped: syncResult.skipped });
}

// Safety guard: prevent committing an empty database when all configs were skipped
Expand All @@ -191,7 +192,7 @@ router.put('/api/configs/ai/bulk', (req, res) => {
res.json({ synced: syncResult.inserted, skipped: syncResult.skipped.length, errors: syncResult.skipped });
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
console.error('PUT /api/configs/ai/bulk error:', err);
logger.errorFromError('configs.bulkAI', 'PUT /api/configs/ai/bulk error', err);
if (errMsg === 'ALL_CONFIGS_SKIPPED') {
res.status(422).json({
error: 'All AI configs were skipped — check the errors field for per-config reasons',
Expand Down Expand Up @@ -237,7 +238,7 @@ router.put('/api/configs/ai/:id', (req, res) => {

res.json({ id, name, apiType, model, baseUrl, apiKey: maskedKey, isActive: !!isActive, reasoningEffort: reasoningEffort ?? null });
} catch (err) {
console.error('PUT /api/configs/ai error:', err);
logger.errorFromError('configs.updateAI', 'PUT /api/configs/ai error', err);
res.status(500).json({ error: 'Failed to update AI config', code: 'UPDATE_AI_CONFIG_FAILED' });
}
});
Expand All @@ -254,7 +255,7 @@ router.delete('/api/configs/ai/:id', (req, res) => {
}
res.json({ deleted: true });
} catch (err) {
console.error('DELETE /api/configs/ai error:', err);
logger.errorFromError('configs.deleteAI', 'DELETE /api/configs/ai error', err);
res.status(500).json({ error: 'Failed to delete AI config', code: 'DELETE_AI_CONFIG_FAILED' });
}
});
Expand Down Expand Up @@ -294,7 +295,7 @@ router.get('/api/configs/webdav', (req, res) => {
});
res.json(configs);
} catch (err) {
console.error('GET /api/configs/webdav error:', err);
logger.errorFromError('configs.getWebDAV', 'GET /api/configs/webdav error', err);
res.status(500).json({ error: 'Failed to fetch WebDAV configs', code: 'FETCH_WEBDAV_CONFIGS_FAILED' });
}
});
Expand All @@ -316,7 +317,7 @@ router.post('/api/configs/webdav', (req, res) => {

res.status(201).json({ id: result.lastInsertRowid, name, url, username, password: maskPassword(password as string), path, isActive: !!isActive });
} catch (err) {
console.error('POST /api/configs/webdav error:', err);
logger.errorFromError('configs.createWebDAV', 'POST /api/configs/webdav error', err);
res.status(500).json({ error: 'Failed to create WebDAV config', code: 'CREATE_WEBDAV_CONFIG_FAILED' });
}
});
Expand Down Expand Up @@ -365,7 +366,7 @@ router.put('/api/configs/webdav/bulk', (req, res) => {
try {
encryptedPwd = encrypt(String(c.password), config.encryptionKey);
} catch (encErr) {
console.error(`[configs] Failed to encrypt WebDAV password for "${c.name}" (${c.id}), falling back to existing:`, encErr);
logger.errorFromError('configs.encryptWebDAVPwd', `Failed to encrypt WebDAV password for "${c.name}" (${c.id})`, encErr);
encryptedPwd = existingPwds.get(String(c.id)) ?? '';
if (!encryptedPwd) {
syncResult.skipped.push({ id: c.id, name: c.name ?? '', reason: 'encrypt_failed' });
Expand Down Expand Up @@ -395,7 +396,7 @@ router.put('/api/configs/webdav/bulk', (req, res) => {
}

if (syncResult.skipped.length > 0) {
console.warn('[configs] Skipped WebDAV configs with missing passwords:', syncResult.skipped);
logger.warn('configs.bulkWebDAV', 'Skipped WebDAV configs with missing passwords', { skippedCount: syncResult.skipped.length });
}

// Safety guard: prevent committing an empty database when all configs were skipped
Expand All @@ -408,7 +409,7 @@ router.put('/api/configs/webdav/bulk', (req, res) => {
res.json({ synced: syncResult.inserted, skipped: syncResult.skipped.length, errors: syncResult.skipped });
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
console.error('PUT /api/configs/webdav/bulk error:', err);
logger.errorFromError('configs.bulkWebDAV', 'PUT /api/configs/webdav/bulk error', err);
if (errMsg === 'ALL_CONFIGS_SKIPPED') {
res.status(422).json({
error: 'All WebDAV configs were skipped — check the errors field for per-config reasons',
Expand Down Expand Up @@ -453,7 +454,7 @@ router.put('/api/configs/webdav/:id', (req, res) => {

res.json({ id, name, url, username, password: maskedPwd, path, isActive: !!isActive });
} catch (err) {
console.error('PUT /api/configs/webdav error:', err);
logger.errorFromError('configs.updateWebDAV', 'PUT /api/configs/webdav error', err);
res.status(500).json({ error: 'Failed to update WebDAV config', code: 'UPDATE_WEBDAV_CONFIG_FAILED' });
}
});
Expand All @@ -470,7 +471,7 @@ router.delete('/api/configs/webdav/:id', (req, res) => {
}
res.json({ deleted: true });
} catch (err) {
console.error('DELETE /api/configs/webdav error:', err);
logger.errorFromError('configs.deleteWebDAV', 'DELETE /api/configs/webdav error', err);
res.status(500).json({ error: 'Failed to delete WebDAV config', code: 'DELETE_WEBDAV_CONFIG_FAILED' });
}
});
Expand Down Expand Up @@ -503,7 +504,7 @@ router.get('/api/settings', (_req, res) => {

res.json(settings);
} catch (err) {
console.error('GET /api/settings error:', err);
logger.errorFromError('configs.getSettings', 'GET /api/settings error', err);
res.status(500).json({ error: 'Failed to fetch settings', code: 'FETCH_SETTINGS_FAILED' });
}
});
Expand Down Expand Up @@ -543,7 +544,7 @@ router.put('/api/settings', (req, res) => {
upsert();
res.json({ updated: true });
} catch (err) {
console.error('PUT /api/settings error:', err);
logger.errorFromError('configs.updateSettings', 'PUT /api/settings error', err);
res.status(500).json({ error: 'Failed to update settings', code: 'UPDATE_SETTINGS_FAILED' });
}
});
Expand Down
27 changes: 27 additions & 0 deletions server/src/routes/logs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Router } from 'express';
import { logger, LogLevel } from '../services/logger.js';

const router = Router();

// GET /api/logs — returns recent backend log entries
router.get('/api/logs', (req, res) => {
try {
const limit = Math.min(parseInt(req.query.limit as string) || 1000, 2000);
const level = req.query.level as LogLevel | undefined;
const since = req.query.since as string | undefined;

// Get all matching entries first for count, then apply limit
const allEntries = logger.getEntries({ level, since });
const total = allEntries.length;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const entries = limit > 0 ? allEntries.slice(-limit) : allEntries;

// Include total count as header for efficient client-side count queries
res.setHeader('X-Log-Count', String(total));
res.json(entries);
} catch (err) {
logger.errorFromError('logs.getLogs', 'Failed to fetch logs', err);
res.status(500).json({ error: 'Failed to fetch logs', code: 'FETCH_LOGS_FAILED' });
}
});

export default router;
17 changes: 9 additions & 8 deletions server/src/routes/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getDb } from '../db/connection.js';
import { encrypt, decrypt } from '../services/crypto.js';
import { config } from '../config.js';
import { proxyRequest, ProxyConfig } from '../services/proxyService.js';
import { logger } from '../services/logger.js';

function getProxyConfig(): ProxyConfig | null {
try {
Expand Down Expand Up @@ -101,7 +102,7 @@ router.post('/api/proxy/github/*', async (req, res) => {
const result = await proxyRequest({ url: targetUrl, method, headers, proxyConfig });
res.status(result.status).json(result.data);
} catch (err) {
console.error('GitHub proxy error:', err);
logger.errorFromError('proxy.github', 'GitHub proxy error', err);
res.status(500).json({ error: 'GitHub proxy failed', code: 'GITHUB_PROXY_FAILED' });
}
});
Expand Down Expand Up @@ -143,7 +144,7 @@ router.post('/api/proxy/ai', async (req, res) => {
try {
const parsed = new URL(baseUrl);
if (parsed.protocol !== 'https:') {
console.warn(`[Proxy] ⚠️ AI API key transmitted over ${parsed.protocol} (not HTTPS). Consider using HTTPS for security.`);
logger.warn('proxy.ai', `AI API key transmitted over ${parsed.protocol} (not HTTPS). Consider using HTTPS for security.`);
}
} catch { /* invalid URL, will be caught by validateUrl later */ }
} else if (configId) {
Expand All @@ -161,7 +162,7 @@ router.post('/api/proxy/ai', async (req, res) => {
try {
const parsed = new URL(baseUrl);
if (parsed.protocol !== 'https:') {
console.warn(`[Proxy] ⚠️ AI API key transmitted over ${parsed.protocol} (not HTTPS). Consider using HTTPS for security.`);
logger.warn('proxy.ai', `AI API key transmitted over ${parsed.protocol} (not HTTPS). Consider using HTTPS for security.`);
}
} catch { /* invalid URL, will be caught by validateUrl later */ }
} else {
Expand Down Expand Up @@ -220,7 +221,7 @@ router.post('/api/proxy/ai', async (req, res) => {

res.status(result.status).json(result.data);
} catch (err) {
console.error('AI proxy error:', err);
logger.errorFromError('proxy.ai', 'AI proxy error', err);
res.status(500).json({ error: 'AI proxy failed', code: 'AI_PROXY_FAILED' });
}
});
Expand Down Expand Up @@ -277,7 +278,7 @@ router.post('/api/proxy/webdav', async (req, res) => {

res.status(result.status).json(result.data);
} catch (err) {
console.error('WebDAV proxy error:', err);
logger.errorFromError('proxy.webdav', 'WebDAV proxy error', err);
res.status(500).json({ error: 'WebDAV proxy failed', code: 'WEBDAV_PROXY_FAILED' });
}
});
Expand Down Expand Up @@ -317,7 +318,7 @@ router.post('/api/proxy/github/search/repositories', async (req, res) => {
const result = await proxyRequest({ url: targetUrl, method: 'GET', headers, proxyConfig });
res.status(result.status).json(result.data);
} catch (err) {
console.error('GitHub search repositories proxy error:', err);
logger.errorFromError('proxy.github.search', 'GitHub search repositories proxy error', err);
res.status(500).json({ error: 'GitHub search proxy failed', code: 'GITHUB_SEARCH_PROXY_FAILED' });
}
});
Expand Down Expand Up @@ -357,7 +358,7 @@ router.post('/api/proxy/github/search/users', async (req, res) => {
const result = await proxyRequest({ url: targetUrl, method: 'GET', headers, proxyConfig });
res.status(result.status).json(result.data);
} catch (err) {
console.error('GitHub search users proxy error:', err);
logger.errorFromError('proxy.github.search', 'GitHub search users proxy error', err);
res.status(500).json({ error: 'GitHub search proxy failed', code: 'GITHUB_SEARCH_PROXY_FAILED' });
}
});
Expand Down Expand Up @@ -416,7 +417,7 @@ router.put('/api/settings/proxy', (req, res) => {

res.json({ success: true });
} catch (err) {
console.error('Failed to save proxy config:', err);
logger.errorFromError('proxy.settings', 'Failed to save proxy config', err);
res.status(500).json({ error: 'Failed to save proxy config' });
}
});
Expand Down
Loading
Loading