Spaces:
Sleeping
Sleeping
File size: 6,389 Bytes
57a889c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | import { Request } from 'express';
import { db } from '../db/database';
import fs from 'fs';
import path from 'path';
const LOG_LEVEL = (process.env.LOG_LEVEL || 'info').toLowerCase();
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10 MB
const MAX_LOG_FILES = 5;
const C = {
blue: '\x1b[34m',
cyan: '\x1b[36m',
red: '\x1b[31m',
yellow: '\x1b[33m',
reset: '\x1b[0m',
};
// ββ File logger with rotation βββββββββββββββββββββββββββββββββββββββββββββ
const logsDir = path.join(process.cwd(), 'data/logs');
try { fs.mkdirSync(logsDir, { recursive: true }); } catch {}
const logFilePath = path.join(logsDir, 'trek.log');
function rotateIfNeeded(): void {
try {
if (!fs.existsSync(logFilePath)) return;
const stat = fs.statSync(logFilePath);
if (stat.size < MAX_LOG_SIZE) return;
for (let i = MAX_LOG_FILES - 1; i >= 1; i--) {
const src = i === 1 ? logFilePath : `${logFilePath}.${i - 1}`;
const dst = `${logFilePath}.${i}`;
if (fs.existsSync(src)) fs.renameSync(src, dst);
}
} catch {}
}
function writeToFile(line: string): void {
try {
rotateIfNeeded();
fs.appendFileSync(logFilePath, line + '\n');
} catch {}
}
// ββ Public log helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββ
function formatTs(): string {
const tz = process.env.TZ || 'UTC';
return new Date().toLocaleString('sv-SE', { timeZone: tz }).replace(' ', 'T');
}
function logInfo(msg: string): void {
const ts = formatTs();
console.log(`${C.blue}[INFO]${C.reset} ${ts} ${msg}`);
writeToFile(`[INFO] ${ts} ${msg}`);
}
function logDebug(msg: string): void {
if (LOG_LEVEL !== 'debug') return;
const ts = formatTs();
console.log(`${C.cyan}[DEBUG]${C.reset} ${ts} ${msg}`);
writeToFile(`[DEBUG] ${ts} ${msg}`);
}
function logError(msg: string): void {
const ts = formatTs();
console.error(`${C.red}[ERROR]${C.reset} ${ts} ${msg}`);
writeToFile(`[ERROR] ${ts} ${msg}`);
}
function logWarn(msg: string): void {
const ts = formatTs();
console.warn(`${C.yellow}[WARN]${C.reset} ${ts} ${msg}`);
writeToFile(`[WARN] ${ts} ${msg}`);
}
// ββ IP + audit ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function getClientIp(req: Request): string | null {
const xff = req.headers['x-forwarded-for'];
if (typeof xff === 'string') {
const first = xff.split(',')[0]?.trim();
return first || null;
}
if (Array.isArray(xff) && xff[0]) return String(xff[0]).trim() || null;
return req.socket?.remoteAddress || null;
}
function resolveUserEmail(userId: number | null): string {
if (!userId) return 'anonymous';
try {
const row = db.prepare('SELECT email FROM users WHERE id = ?').get(userId) as { email: string } | undefined;
return row?.email || `uid:${userId}`;
} catch { return `uid:${userId}`; }
}
const ACTION_LABELS: Record<string, string> = {
'user.register': 'registered',
'user.login': 'logged in',
'user.login_failed': 'login failed',
'user.password_change': 'changed password',
'user.account_delete': 'deleted account',
'user.mfa_enable': 'enabled MFA',
'user.mfa_disable': 'disabled MFA',
'settings.app_update': 'updated settings',
'trip.create': 'created trip',
'trip.delete': 'deleted trip',
'admin.user_role_change': 'changed user role',
'admin.user_delete': 'deleted user',
'admin.invite_create': 'created invite',
'immich.private_ip_configured': 'configured Immich with private IP',
};
/** Best-effort; never throws β failures are logged only. */
export function writeAudit(entry: {
userId: number | null;
action: string;
resource?: string | null;
details?: Record<string, unknown>;
debugDetails?: Record<string, unknown>;
ip?: string | null;
}): void {
try {
const detailsJson = entry.details && Object.keys(entry.details).length > 0 ? JSON.stringify(entry.details) : null;
db.prepare(
`INSERT INTO audit_log (user_id, action, resource, details, ip) VALUES (?, ?, ?, ?, ?)`
).run(entry.userId, entry.action, entry.resource ?? null, detailsJson, entry.ip ?? null);
const email = resolveUserEmail(entry.userId);
const label = ACTION_LABELS[entry.action] || entry.action;
const brief = buildInfoSummary(entry.action, entry.details);
logInfo(`${email} ${label}${brief} ip=${entry.ip || '-'}`);
if (entry.debugDetails && Object.keys(entry.debugDetails).length > 0) {
logDebug(`AUDIT ${entry.action} userId=${entry.userId} ${JSON.stringify(entry.debugDetails)}`);
} else if (detailsJson) {
logDebug(`AUDIT ${entry.action} userId=${entry.userId} ${detailsJson}`);
}
} catch (e) {
logError(`Audit write failed: ${e instanceof Error ? e.message : e}`);
}
}
function buildInfoSummary(action: string, details?: Record<string, unknown>): string {
if (!details || Object.keys(details).length === 0) return '';
if (action === 'trip.create') return ` "${details.title}"`;
if (action === 'trip.delete') return ` tripId=${details.tripId}`;
if (action === 'user.register') return ` ${details.email}`;
if (action === 'user.login') return '';
if (action === 'user.login_failed') return ` reason=${details.reason}`;
if (action === 'settings.app_update') {
const parts: string[] = [];
if (details.notification_channel) parts.push(`channel=${details.notification_channel}`);
if (details.smtp_settings_updated) parts.push('smtp');
if (details.notification_events_updated) parts.push('events');
if (details.webhook_url_updated) parts.push('webhook_url');
if (details.allowed_file_types_updated) parts.push('file_types');
if (details.allow_registration !== undefined) parts.push(`registration=${details.allow_registration}`);
if (details.require_mfa !== undefined) parts.push(`mfa=${details.require_mfa}`);
return parts.length ? ` (${parts.join(', ')})` : '';
}
if (action === 'immich.private_ip_configured') {
return details.resolved_ip ? ` url=${details.immich_url} ip=${details.resolved_ip}` : '';
}
return '';
}
export { LOG_LEVEL, logInfo, logDebug, logError, logWarn };
|