Skip to content

Commit da3bf71

Browse files
authored
Merge pull request #58 from CoolerMinecraft/stuff
Stuff
2 parents 7dcef66 + 003dba1 commit da3bf71

22 files changed

Lines changed: 1751 additions & 611 deletions

File tree

server/db/flightLogs.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { mainDb } from "./connection.js";
2+
import { encrypt, decrypt } from "../utils/encryption.js"; // Assuming decrypt function exists
3+
import { sql } from "kysely";
4+
5+
export interface FlightLogData {
6+
userId: string;
7+
username: string;
8+
sessionId: string;
9+
action: 'add' | 'update' | 'delete';
10+
flightId: string;
11+
oldData?: object | null;
12+
newData?: object | null;
13+
ipAddress?: string | null;
14+
}
15+
16+
export async function logFlightAction(logData: FlightLogData) {
17+
const {
18+
userId,
19+
username,
20+
sessionId,
21+
action,
22+
flightId,
23+
oldData = null,
24+
newData = null,
25+
ipAddress = null
26+
} = logData;
27+
28+
try {
29+
const encryptedIP = ipAddress ? JSON.stringify(encrypt(ipAddress)) : null;
30+
await mainDb
31+
.insertInto('flight_logs')
32+
.values({
33+
id: sql`DEFAULT`,
34+
user_id: userId,
35+
username,
36+
session_id: sessionId,
37+
action,
38+
flight_id: flightId,
39+
old_data: oldData,
40+
new_data: newData,
41+
ip_address: encryptedIP,
42+
timestamp: sql`NOW()`
43+
})
44+
.execute();
45+
} catch (error) {
46+
console.error('Error logging flight action:', error);
47+
// Non-blocking: don't throw, just log
48+
}
49+
}
50+
51+
export async function cleanupOldFlightLogs(daysToKeep = 30) {
52+
try {
53+
const result = await mainDb
54+
.deleteFrom('flight_logs')
55+
.where('timestamp', '<', new Date(Date.now() - daysToKeep * 24 * 60 * 60 * 1000))
56+
.executeTakeFirst();
57+
58+
return Number(result?.numDeletedRows ?? 0);
59+
} catch (error) {
60+
console.error('Error cleaning up flight logs:', error);
61+
throw error;
62+
}
63+
}
64+
65+
let cleanupInterval: NodeJS.Timeout | null = null;
66+
67+
export function startFlightLogsCleanup() {
68+
const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; // Daily
69+
70+
// Initial cleanup after 1 minute
71+
setTimeout(async () => {
72+
try {
73+
await cleanupOldFlightLogs(30);
74+
} catch (error) {
75+
console.error('Initial flight logs cleanup failed:', error);
76+
}
77+
}, 60 * 1000);
78+
79+
// Recurring cleanup
80+
cleanupInterval = setInterval(async () => {
81+
try {
82+
await cleanupOldFlightLogs(30);
83+
} catch (error) {
84+
console.error('Scheduled flight logs cleanup failed:', error);
85+
}
86+
}, CLEANUP_INTERVAL);
87+
}
88+
89+
export function stopFlightLogsCleanup() {
90+
if (cleanupInterval) {
91+
clearInterval(cleanupInterval);
92+
cleanupInterval = null;
93+
}
94+
}
95+
96+
export interface FlightLogFilters {
97+
user?: string;
98+
action?: 'add' | 'update' | 'delete';
99+
session?: string;
100+
flightId?: string;
101+
dateFrom?: string;
102+
dateTo?: string;
103+
}
104+
105+
export async function getFlightLogs(
106+
page = 1,
107+
limit = 50,
108+
filters: FlightLogFilters = {}
109+
) {
110+
try {
111+
let query = mainDb
112+
.selectFrom('flight_logs')
113+
.selectAll()
114+
.orderBy('timestamp', 'desc')
115+
.limit(limit)
116+
.offset((page - 1) * limit);
117+
118+
if (filters.user) {
119+
query = query.where('username', 'ilike', `%${filters.user}%`);
120+
}
121+
if (filters.action) {
122+
query = query.where('action', '=', filters.action);
123+
}
124+
if (filters.session) {
125+
query = query.where('session_id', '=', filters.session);
126+
}
127+
if (filters.flightId) {
128+
query = query.where('flight_id', '=', filters.flightId);
129+
}
130+
if (filters.dateFrom) {
131+
query = query.where('timestamp', '>=', new Date(filters.dateFrom));
132+
}
133+
if (filters.dateTo) {
134+
query = query.where('timestamp', '<=', new Date(filters.dateTo));
135+
}
136+
137+
const logs = await query.execute();
138+
const total = await mainDb
139+
.selectFrom('flight_logs')
140+
.select((eb) => eb.fn.count('id').as('count'))
141+
.executeTakeFirst();
142+
143+
return {
144+
logs: logs.map(log => ({
145+
...log,
146+
ip_address: log.ip_address ? decrypt(JSON.parse(log.ip_address)) : null,
147+
})),
148+
pagination: {
149+
page,
150+
limit,
151+
total: Number(total?.count || 0),
152+
pages: Math.ceil(Number(total?.count || 0) / limit),
153+
},
154+
};
155+
} catch (error) {
156+
console.error('Error fetching flight logs:', error);
157+
throw error;
158+
}
159+
}
160+
161+
export async function getFlightLogById(logId: string | number) {
162+
try {
163+
const log = await mainDb
164+
.selectFrom('flight_logs')
165+
.selectAll()
166+
.where('id', '=', Number(logId))
167+
.executeTakeFirst();
168+
169+
if (!log) return null;
170+
171+
return {
172+
...log,
173+
ip_address: log.ip_address ? decrypt(JSON.parse(log.ip_address)) : null,
174+
};
175+
} catch (error) {
176+
console.error('Error fetching flight log by ID:', error);
177+
throw error;
178+
}
179+
}

server/db/roles.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { mainDb } from "./connection.js";
22
import { sql } from "kysely";
33
import { isAdmin } from "../middleware/admin.js";
44
import { invalidateAllUsersCache } from './admin.js';
5+
import { invalidateUserCache } from './users.js'
56

67
export async function getAllRoles() {
78
try {
@@ -159,6 +160,7 @@ export async function assignRoleToUser(userId: string, roleId: number) {
159160
}
160161

161162
await invalidateAllUsersCache();
163+
await invalidateUserCache(userId);
162164

163165
return { userId, roleId };
164166
} catch (error) {
@@ -189,6 +191,7 @@ export async function removeRoleFromUser(userId: string, roleId: number) {
189191
}
190192

191193
await invalidateAllUsersCache();
194+
await invalidateUserCache(userId);
192195

193196
return { userId, roleId };
194197
} catch (error) {

server/db/schemas.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,22 @@ export async function createMainTables() {
151151
.addColumn('created_at', 'timestamp', (col) => col.defaultTo('now()'))
152152
.addColumn('updated_at', 'timestamp', (col) => col.defaultTo('now()'))
153153
.execute();
154+
155+
// flight_logs
156+
await mainDb.schema
157+
.createTable('flight_logs')
158+
.ifNotExists()
159+
.addColumn('id', 'serial', (col) => col.primaryKey())
160+
.addColumn('user_id', 'varchar(255)', (col) => col.notNull())
161+
.addColumn('username', 'varchar(255)', (col) => col.notNull())
162+
.addColumn('session_id', 'varchar(255)', (col) => col.notNull())
163+
.addColumn('action', 'varchar(50)', (col) => col.notNull())
164+
.addColumn('flight_id', 'varchar(255)', (col) => col.notNull())
165+
.addColumn('old_data', 'jsonb')
166+
.addColumn('new_data', 'jsonb')
167+
.addColumn('ip_address', 'varchar(255)')
168+
.addColumn('timestamp', 'timestamp', (col) => col.defaultTo('now()'))
169+
.execute();
154170
}
155171

156172
// Helper to create a dynamic flights table for a session

server/db/types/connection/MainDatabase.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { TesterSettingsTable } from "./main/TesterSettingsTable";
1212
import { DailyStatisticsTable } from "./main/DailyStatisticsTable";
1313
import { ChatReportsTable } from "./main/ChatReportsTable";
1414
import { UpdateModalsTable } from "./main/UpdateModalsTable";
15+
import { FlightLogsTable } from "./main/FlightLogsTable";
1516

1617
export interface MainDatabase {
1718
app_settings: AppSettingsTable;
@@ -28,4 +29,5 @@ export interface MainDatabase {
2829
daily_statistics: DailyStatisticsTable;
2930
chat_report: ChatReportsTable;
3031
update_modals: UpdateModalsTable;
32+
flight_logs: FlightLogsTable;
3133
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export interface FlightLogsTable {
2+
id: number;
3+
user_id: string;
4+
username: string;
5+
session_id: string;
6+
action: 'add' | 'update' | 'delete';
7+
flight_id: string;
8+
old_data: object | null;
9+
new_data: object | null;
10+
ip_address: string | null;
11+
timestamp: Date;
12+
}

server/db/users.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { mainDb } from "./connection.js";
44
import { sql } from "kysely";
55
import { redisConnection } from "./connection.js";
66
import { incrementStat } from '../utils/statisticsCache.js';
7+
import { getUserRoles } from './roles.js';
78

8-
async function invalidateUserCache(userId: string) {
9+
export async function invalidateUserCache(userId: string) {
910
try {
1011
await redisConnection.del(`user:${userId}`);
1112
} catch (error) {
@@ -40,19 +41,40 @@ export async function getUserById(userId: string) {
4041
.leftJoin("roles", "users.role_id", "roles.id")
4142
.selectAll("users")
4243
.select("roles.permissions as role_permissions")
44+
.select("roles.name as role_name")
4345
.where("users.id", "=", userId)
4446
.executeTakeFirst();
4547

4648
if (!user) return null;
4749

50+
const userRoles = await getUserRoles(userId);
51+
const mergedPermissions: Record<string, boolean> = {};
52+
for (const role of userRoles) {
53+
let perms = role.permissions;
54+
if (typeof perms === 'string') {
55+
try {
56+
perms = JSON.parse(perms);
57+
} catch {
58+
perms = {};
59+
}
60+
}
61+
if (perms && typeof perms === 'object') {
62+
Object.assign(mergedPermissions, perms as Record<string, boolean>);
63+
}
64+
}
65+
66+
if (Object.keys(mergedPermissions).length === 0 && user.role_permissions) {
67+
Object.assign(mergedPermissions, user.role_permissions as Record<string, boolean>);
68+
}
69+
4870
const result = {
4971
...user,
5072
access_token: user.access_token ? decrypt(JSON.parse(user.access_token)) : null,
5173
refresh_token: user.refresh_token ? decrypt(JSON.parse(user.refresh_token)) : null,
5274
sessions: user.sessions ? decrypt(JSON.parse(user.sessions)) : null,
5375
settings: user.settings ? decrypt(JSON.parse(user.settings)) : null,
5476
ip_address: user.ip_address ? decrypt(JSON.parse(user.ip_address)) : null,
55-
role_permissions: user.role_permissions || null,
77+
role_permissions: mergedPermissions,
5678
statistics: user.statistics || {},
5779
};
5880

server/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { setupArrivalsWebsocket } from './websockets/arrivalsWebsocket.js';
1818

1919
import { startStatsFlushing } from './utils/statisticsCache.js';
2020
import { updateLeaderboard } from './db/leaderboard.js';
21+
import { startFlightLogsCleanup } from './db/flightLogs.js';
2122

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

105106
startStatsFlushing();
107+
startFlightLogsCleanup();
106108
updateLeaderboard();
107109
setInterval(updateLeaderboard, 12 * 60 * 60 * 1000); // 12h
108110

server/middleware/rolePermissions.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1+
import { Request, Response, NextFunction } from 'express';
12
import { getUserById } from '../db/users.js';
2-
import { getRoleById } from '../db/roles.js';
33
import { isAdmin } from './admin.js';
44

5-
import { Request, Response, NextFunction } from 'express';
6-
75
type PermissionKey = string;
86

9-
interface Role {
10-
permissions: { [key: string]: boolean };
11-
}
12-
137
export function requirePermission(permission: PermissionKey) {
148
return async (req: Request, res: Response, next: NextFunction) => {
159
try {
@@ -22,20 +16,29 @@ export function requirePermission(permission: PermissionKey) {
2216
}
2317

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

29-
const dbRole = await getRoleById(user.roleId);
30-
const role: Role | null = dbRole
31-
? {
32-
permissions: (typeof dbRole.permissions === 'object' && dbRole.permissions !== null
33-
? dbRole.permissions
34-
: {}) as { [key: string]: boolean }
23+
const { getUserRoles } = await import('../db/roles.js');
24+
const userRoles = await getUserRoles(user.id);
25+
26+
const mergedPermissions: Record<string, boolean> = {};
27+
for (const role of userRoles) {
28+
let perms = role.permissions;
29+
if (typeof perms === 'string') {
30+
try {
31+
perms = JSON.parse(perms);
32+
} catch {
33+
perms = {};
34+
}
35+
}
36+
if (perms && typeof perms === 'object') {
37+
Object.assign(mergedPermissions, perms as Record<string, boolean>);
3538
}
36-
: null;
39+
}
3740

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

0 commit comments

Comments
 (0)