Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
179 changes: 179 additions & 0 deletions server/db/flightLogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { mainDb } from "./connection.js";
import { encrypt, decrypt } from "../utils/encryption.js"; // Assuming decrypt function exists
import { sql } from "kysely";

export interface FlightLogData {
userId: string;
username: string;
sessionId: string;
action: 'add' | 'update' | 'delete';
flightId: string;
oldData?: object | null;
newData?: object | null;
ipAddress?: string | null;
}

export async function logFlightAction(logData: FlightLogData) {
const {
userId,
username,
sessionId,
action,
flightId,
oldData = null,
newData = null,
ipAddress = null
} = logData;

try {
const encryptedIP = ipAddress ? JSON.stringify(encrypt(ipAddress)) : null;
await mainDb
.insertInto('flight_logs')
.values({
id: sql`DEFAULT`,
user_id: userId,
username,
session_id: sessionId,
action,
flight_id: flightId,
old_data: oldData,
new_data: newData,
ip_address: encryptedIP,
timestamp: sql`NOW()`
})
.execute();
} catch (error) {
console.error('Error logging flight action:', error);
// Non-blocking: don't throw, just log
}
}

export async function cleanupOldFlightLogs(daysToKeep = 30) {
try {
const result = await mainDb
.deleteFrom('flight_logs')
.where('timestamp', '<', new Date(Date.now() - daysToKeep * 24 * 60 * 60 * 1000))
.executeTakeFirst();

return Number(result?.numDeletedRows ?? 0);
} catch (error) {
console.error('Error cleaning up flight logs:', error);
throw error;
}
}

let cleanupInterval: NodeJS.Timeout | null = null;

export function startFlightLogsCleanup() {
const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; // Daily

// Initial cleanup after 1 minute
setTimeout(async () => {
try {
await cleanupOldFlightLogs(30);
} catch (error) {
console.error('Initial flight logs cleanup failed:', error);
}
}, 60 * 1000);

// Recurring cleanup
cleanupInterval = setInterval(async () => {
try {
await cleanupOldFlightLogs(30);
} catch (error) {
console.error('Scheduled flight logs cleanup failed:', error);
}
}, CLEANUP_INTERVAL);
}

export function stopFlightLogsCleanup() {
if (cleanupInterval) {
clearInterval(cleanupInterval);
cleanupInterval = null;
}
}

export interface FlightLogFilters {
user?: string;
action?: 'add' | 'update' | 'delete';
session?: string;
flightId?: string;
dateFrom?: string;
dateTo?: string;
}

export async function getFlightLogs(
page = 1,
limit = 50,
filters: FlightLogFilters = {}
) {
try {
let query = mainDb
.selectFrom('flight_logs')
.selectAll()
.orderBy('timestamp', 'desc')
.limit(limit)
.offset((page - 1) * limit);

if (filters.user) {
query = query.where('username', 'ilike', `%${filters.user}%`);
}
if (filters.action) {
query = query.where('action', '=', filters.action);
}
if (filters.session) {
query = query.where('session_id', '=', filters.session);
}
if (filters.flightId) {
query = query.where('flight_id', '=', filters.flightId);
}
if (filters.dateFrom) {
query = query.where('timestamp', '>=', new Date(filters.dateFrom));
}
if (filters.dateTo) {
query = query.where('timestamp', '<=', new Date(filters.dateTo));
}

const logs = await query.execute();
const total = await mainDb
.selectFrom('flight_logs')
.select((eb) => eb.fn.count('id').as('count'))
.executeTakeFirst();

return {
logs: logs.map(log => ({
...log,
ip_address: log.ip_address ? decrypt(JSON.parse(log.ip_address)) : null,
})),
pagination: {
page,
limit,
total: Number(total?.count || 0),
pages: Math.ceil(Number(total?.count || 0) / limit),
},
};
} catch (error) {
console.error('Error fetching flight logs:', error);
throw error;
}
}

export async function getFlightLogById(logId: string | number) {
try {
const log = await mainDb
.selectFrom('flight_logs')
.selectAll()
.where('id', '=', Number(logId))
.executeTakeFirst();

if (!log) return null;

return {
...log,
ip_address: log.ip_address ? decrypt(JSON.parse(log.ip_address)) : null,
};
} catch (error) {
console.error('Error fetching flight log by ID:', error);
throw error;
}
}
3 changes: 3 additions & 0 deletions server/db/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { mainDb } from "./connection.js";
import { sql } from "kysely";
import { isAdmin } from "../middleware/admin.js";
import { invalidateAllUsersCache } from './admin.js';
import { invalidateUserCache } from './users.js'

export async function getAllRoles() {
try {
Expand Down Expand Up @@ -159,6 +160,7 @@ export async function assignRoleToUser(userId: string, roleId: number) {
}

await invalidateAllUsersCache();
await invalidateUserCache(userId);

return { userId, roleId };
} catch (error) {
Expand Down Expand Up @@ -189,6 +191,7 @@ export async function removeRoleFromUser(userId: string, roleId: number) {
}

await invalidateAllUsersCache();
await invalidateUserCache(userId);

return { userId, roleId };
} catch (error) {
Expand Down
16 changes: 16 additions & 0 deletions server/db/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,22 @@ export async function createMainTables() {
.addColumn('created_at', 'timestamp', (col) => col.defaultTo('now()'))
.addColumn('updated_at', 'timestamp', (col) => col.defaultTo('now()'))
.execute();

// flight_logs
await mainDb.schema
.createTable('flight_logs')
.ifNotExists()
.addColumn('id', 'serial', (col) => col.primaryKey())
.addColumn('user_id', 'varchar(255)', (col) => col.notNull())
.addColumn('username', 'varchar(255)', (col) => col.notNull())
.addColumn('session_id', 'varchar(255)', (col) => col.notNull())
.addColumn('action', 'varchar(50)', (col) => col.notNull())
.addColumn('flight_id', 'varchar(255)', (col) => col.notNull())
.addColumn('old_data', 'jsonb')
.addColumn('new_data', 'jsonb')
.addColumn('ip_address', 'varchar(255)')
.addColumn('timestamp', 'timestamp', (col) => col.defaultTo('now()'))
.execute();
}

// Helper to create a dynamic flights table for a session
Expand Down
2 changes: 2 additions & 0 deletions server/db/types/connection/MainDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { TesterSettingsTable } from "./main/TesterSettingsTable";
import { DailyStatisticsTable } from "./main/DailyStatisticsTable";
import { ChatReportsTable } from "./main/ChatReportsTable";
import { UpdateModalsTable } from "./main/UpdateModalsTable";
import { FlightLogsTable } from "./main/FlightLogsTable";

export interface MainDatabase {
app_settings: AppSettingsTable;
Expand All @@ -28,4 +29,5 @@ export interface MainDatabase {
daily_statistics: DailyStatisticsTable;
chat_report: ChatReportsTable;
update_modals: UpdateModalsTable;
flight_logs: FlightLogsTable;
}
12 changes: 12 additions & 0 deletions server/db/types/connection/main/FlightLogsTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface FlightLogsTable {
id: number;
user_id: string;
username: string;
session_id: string;
action: 'add' | 'update' | 'delete';
flight_id: string;
old_data: object | null;
new_data: object | null;
ip_address: string | null;
timestamp: Date;
}
26 changes: 24 additions & 2 deletions server/db/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { mainDb } from "./connection.js";
import { sql } from "kysely";
import { redisConnection } from "./connection.js";
import { incrementStat } from '../utils/statisticsCache.js';
import { getUserRoles } from './roles.js';

async function invalidateUserCache(userId: string) {
export async function invalidateUserCache(userId: string) {
try {
await redisConnection.del(`user:${userId}`);
} catch (error) {
Expand Down Expand Up @@ -40,19 +41,40 @@ export async function getUserById(userId: string) {
.leftJoin("roles", "users.role_id", "roles.id")
.selectAll("users")
.select("roles.permissions as role_permissions")
.select("roles.name as role_name")
.where("users.id", "=", userId)
.executeTakeFirst();

if (!user) return null;

const userRoles = await getUserRoles(userId);
const mergedPermissions: Record<string, boolean> = {};
for (const role of userRoles) {
let perms = role.permissions;
if (typeof perms === 'string') {
try {
perms = JSON.parse(perms);
} catch {
perms = {};
}
}
if (perms && typeof perms === 'object') {
Object.assign(mergedPermissions, perms as Record<string, boolean>);
}
}

if (Object.keys(mergedPermissions).length === 0 && user.role_permissions) {
Object.assign(mergedPermissions, user.role_permissions as Record<string, boolean>);
}

const result = {
...user,
access_token: user.access_token ? decrypt(JSON.parse(user.access_token)) : null,
refresh_token: user.refresh_token ? decrypt(JSON.parse(user.refresh_token)) : null,
sessions: user.sessions ? decrypt(JSON.parse(user.sessions)) : null,
settings: user.settings ? decrypt(JSON.parse(user.settings)) : null,
ip_address: user.ip_address ? decrypt(JSON.parse(user.ip_address)) : null,
role_permissions: user.role_permissions || null,
role_permissions: mergedPermissions,
statistics: user.statistics || {},
};

Expand Down
2 changes: 2 additions & 0 deletions server/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { setupArrivalsWebsocket } from './websockets/arrivalsWebsocket.js';

import { startStatsFlushing } from './utils/statisticsCache.js';
import { updateLeaderboard } from './db/leaderboard.js';
import { startFlightLogsCleanup } from './db/flightLogs.js';

dotenv.config({ path: process.env.NODE_ENV === 'production' ? '.env.production' : '.env.development' });
console.log(chalk.bgBlue('NODE_ENV:'), process.env.NODE_ENV);
Expand Down Expand Up @@ -103,6 +104,7 @@ const arrivalsIO = setupArrivalsWebsocket(server);
arrivalsIO.adapter(createAdapter(pubClient, subClient));

startStatsFlushing();
startFlightLogsCleanup();
updateLeaderboard();
setInterval(updateLeaderboard, 12 * 60 * 60 * 1000); // 12h

Expand Down
37 changes: 20 additions & 17 deletions server/middleware/rolePermissions.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { Request, Response, NextFunction } from 'express';
import { getUserById } from '../db/users.js';
import { getRoleById } from '../db/roles.js';
import { isAdmin } from './admin.js';

import { Request, Response, NextFunction } from 'express';

type PermissionKey = string;

interface Role {
permissions: { [key: string]: boolean };
}

export function requirePermission(permission: PermissionKey) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
Expand All @@ -22,20 +16,29 @@ export function requirePermission(permission: PermissionKey) {
}

const user = await getUserById(req.user.userId);
if (!user || !user.roleId) {
return res.status(403).json({ error: 'Access denied - insufficient permissions' });
if (!user) {
return res.status(403).json({ error: 'Access denied - user not found' });
}

const dbRole = await getRoleById(user.roleId);
const role: Role | null = dbRole
? {
permissions: (typeof dbRole.permissions === 'object' && dbRole.permissions !== null
? dbRole.permissions
: {}) as { [key: string]: boolean }
const { getUserRoles } = await import('../db/roles.js');
const userRoles = await getUserRoles(user.id);

const mergedPermissions: Record<string, boolean> = {};
for (const role of userRoles) {
let perms = role.permissions;
if (typeof perms === 'string') {
try {
perms = JSON.parse(perms);
} catch {
perms = {};
}
}
if (perms && typeof perms === 'object') {
Object.assign(mergedPermissions, perms as Record<string, boolean>);
}
: null;
}

if (!role || !role.permissions[permission]) {
if (!mergedPermissions[permission]) {
return res.status(403).json({ error: 'Access denied - insufficient permissions' });
}

Expand Down
Loading
Loading