Skip to content

Commit 7148c35

Browse files
authored
Merge pull request #157 from cephie-studios/canary
Canary
2 parents d36f2af + c3a4652 commit 7148c35

34 files changed

+2731
-1580
lines changed

index.html

Lines changed: 48 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,56 @@
11
<!doctype html>
22
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>PFControl v2</title>
8-
<meta
9-
name="description"
10-
content="PFControl is the next-generation flight strip platform for real-time coordination between air traffic controllers. Secure, reliable, and collaborative."
11-
/>
123

13-
<meta
14-
name="keywords"
15-
content="PFControl, Project Flight, PTFS, Pilot Training Flight Simulator, Roblox aviation, flight strips, air traffic control, ATC, real-time, aviation games, collaborative, sessions, pilots, controllers, Roblox ATC, Project Flight ATC, aviation simulator, flight control, tower control"
16-
/>
17-
<meta name="author" content="PFConnect Studios" />
4+
<head>
5+
<meta charset="UTF-8" />
6+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<title>PFControl v2</title>
9+
<meta name="description"
10+
content="PFControl is the next-generation flight strip platform for real-time coordination between air traffic controllers. Secure, reliable, and collaborative." />
1811

19-
<meta
20-
property="og:title"
21-
content="PFControl v2 - Flight Strips for Project Flight & Roblox Aviation"
22-
/>
23-
<meta
24-
property="og:description"
25-
content="The next-generation flight strip platform built for real-time coordination between air traffic controllers in Project Flight, PTFS, and Roblox aviation games with enterprise-level reliability."
26-
/>
27-
<meta property="og:type" content="website" />
28-
<meta property="og:url" content="https://pfcontrol.com/" />
12+
<meta name="keywords"
13+
content="PFControl, Project Flight, PTFS, Pilot Training Flight Simulator, Roblox aviation, flight strips, air traffic control, ATC, real-time, aviation games, collaborative, sessions, pilots, controllers, Roblox ATC, Project Flight ATC, aviation simulator, flight control, tower control" />
14+
<meta name="author" content="Cephie Studios" />
2915

30-
<meta name="twitter:card" content="summary_large_image" />
31-
<meta
32-
name="twitter:title"
33-
content="PFControl v2 - Flight Strips for Project Flight & Roblox Aviation"
34-
/>
35-
<meta
36-
name="twitter:description"
37-
content="Professional flight strip platform for Project Flight, PTFS, and other Roblox aviation games. Real-time ATC coordination made easy."
38-
/>
16+
<meta property="og:title" content="PFControl v2 - Flight Strips for Project Flight & Roblox Aviation" />
17+
<meta property="og:description"
18+
content="The next-generation flight strip platform built for real-time coordination between air traffic controllers in Project Flight, PTFS, and Roblox aviation games with enterprise-level reliability." />
19+
<meta property="og:type" content="website" />
20+
<meta property="og:url" content="https://pfcontrol.com/" />
3921

40-
<meta name="robots" content="index, follow" />
41-
<meta name="googlebot" content="index, follow" />
42-
<meta name="google-adsense-account" content="ca-pub-3075420086521736">
43-
<link rel="canonical" href="https://pfcontrol.com/" />
44-
<link
45-
rel="sitemap"
46-
type="application/xml"
47-
title="Sitemap"
48-
href="/sitemap.xml"
49-
/>
22+
<meta name="twitter:card" content="summary_large_image" />
23+
<meta name="twitter:title" content="PFControl v2 - Flight Strips for Project Flight & Roblox Aviation" />
24+
<meta name="twitter:description"
25+
content="Professional flight strip platform for Project Flight, PTFS, and other Roblox aviation games. Real-time ATC coordination made easy." />
5026

51-
<script type="application/ld+json">
52-
{
53-
"@context": "https://schema.org",
54-
"@type": "WebApplication",
55-
"name": "PFControl v2",
56-
"description": "Flight strip platform for Project Flight and Roblox aviation games",
57-
"url": "https://pfcontrol.com/",
58-
"applicationCategory": "Gaming, Aviation Simulation",
59-
"operatingSystem": "Web Browser",
60-
"keywords": "Project Flight, PTFS, Roblox aviation, flight strips, ATC",
61-
"author": {
62-
"@type": "Organization",
63-
"name": "PFConnect Studios"
64-
}
27+
<meta name="robots" content="index, follow" />
28+
<meta name="googlebot" content="index, follow" />
29+
<meta name="google-adsense-account" content="ca-pub-3075420086521736">
30+
<link rel="canonical" href="https://pfcontrol.com/" />
31+
<link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml" />
32+
33+
<script type="application/ld+json">
34+
{
35+
"@context": "https://schema.org",
36+
"@type": "WebApplication",
37+
"name": "PFControl v2",
38+
"description": "Flight strip platform for Project Flight and Roblox aviation games",
39+
"url": "https://pfcontrol.com/",
40+
"applicationCategory": "Gaming, Aviation Simulation",
41+
"operatingSystem": "Web Browser",
42+
"keywords": "Project Flight, PTFS, Roblox aviation, flight strips, ATC",
43+
"author": {
44+
"@type": "Organization",
45+
"name": "Cephie Studios"
6546
}
66-
</script>
67-
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3075420086521736" crossorigin="anonymous"></script>
68-
</head>
69-
<body>
70-
<div id="root"></div>
71-
<script type="module" src="/src/main.tsx"></script>
72-
</body>
73-
</html>
47+
}
48+
</script>
49+
</head>
50+
51+
<body>
52+
<div id="root"></div>
53+
<script type="module" src="/src/main.tsx"></script>
54+
</body>
55+
56+
</html>

server/db/audit.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,13 @@ export async function getAuditLogs(
129129
let decryptedIP = null;
130130
if (log.ip_address) {
131131
try {
132-
const parsed = JSON.parse(log.ip_address as string);
132+
const parsed =
133+
typeof log.ip_address === 'string'
134+
? JSON.parse(log.ip_address)
135+
: log.ip_address;
133136
decryptedIP = decrypt(parsed);
134137
} catch {
135-
try {
136-
decryptedIP = decrypt(JSON.parse(log.ip_address as string));
137-
} catch {
138-
decryptedIP = null;
139-
}
138+
decryptedIP = null;
140139
}
141140
}
142141
return {
@@ -231,14 +230,13 @@ export async function getAuditLogById(logId: number | string) {
231230
let decryptedIP = null;
232231
if (result.ip_address) {
233232
try {
234-
const parsed = JSON.parse(result.ip_address as string);
233+
const parsed =
234+
typeof result.ip_address === 'string'
235+
? JSON.parse(result.ip_address)
236+
: result.ip_address;
235237
decryptedIP = decrypt(parsed);
236238
} catch {
237-
try {
238-
decryptedIP = decrypt(JSON.parse(result.ip_address as string));
239-
} catch {
240-
decryptedIP = null;
241-
}
239+
decryptedIP = null;
242240
}
243241
}
244242

server/db/flightLogs.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { mainDb } from './connection.js';
22
import { encrypt, decrypt } from '../utils/encryption.js';
33
import { sql } from 'kysely';
44

5+
const FLIGHT_LOG_RETENTION_DAYS = 365;
6+
57
export interface FlightLogData {
68
userId: string;
79
username: string;
@@ -60,7 +62,9 @@ export async function logFlightAction(logData: FlightLogData) {
6062
}
6163
}
6264

63-
export async function cleanupOldFlightLogs(daysToKeep = 30) {
65+
export async function cleanupOldFlightLogs(
66+
daysToKeep = FLIGHT_LOG_RETENTION_DAYS
67+
) {
6468
try {
6569
const result = await mainDb
6670
.deleteFrom('flight_logs')
@@ -85,15 +89,15 @@ export function startFlightLogsCleanup() {
8589

8690
setTimeout(async () => {
8791
try {
88-
await cleanupOldFlightLogs(30);
92+
await cleanupOldFlightLogs(FLIGHT_LOG_RETENTION_DAYS);
8993
} catch (error) {
9094
console.error('Initial flight logs cleanup failed:', error);
9195
}
9296
}, 60 * 1000);
9397

9498
cleanupInterval = setInterval(async () => {
9599
try {
96-
await cleanupOldFlightLogs(30);
100+
await cleanupOldFlightLogs(FLIGHT_LOG_RETENTION_DAYS);
97101
} catch (error) {
98102
console.error('Scheduled flight logs cleanup failed:', error);
99103
}
@@ -229,7 +233,13 @@ export async function getFlightLogs(
229233
return {
230234
logs: logs.map((log) => ({
231235
...log,
232-
ip_address: log.ip_address ? decrypt(JSON.parse(log.ip_address)) : null,
236+
ip_address: log.ip_address
237+
? decrypt(
238+
typeof log.ip_address === 'string'
239+
? JSON.parse(log.ip_address)
240+
: log.ip_address
241+
)
242+
: null,
233243
})),
234244
pagination: {
235245
page,
@@ -256,7 +266,13 @@ export async function getFlightLogById(logId: string | number) {
256266

257267
return {
258268
...log,
259-
ip_address: log.ip_address ? decrypt(JSON.parse(log.ip_address)) : null,
269+
ip_address: log.ip_address
270+
? decrypt(
271+
typeof log.ip_address === 'string'
272+
? JSON.parse(log.ip_address)
273+
: log.ip_address
274+
)
275+
: null,
260276
};
261277
} catch (error) {
262278
console.error('Error fetching flight log by ID:', error);

server/db/flights.ts

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import crypto from 'crypto';
1111
import { sql } from 'kysely';
1212
import { incrementStat } from '../utils/statisticsCache.js';
1313
import type { FlightsTable } from './types/connection/main/FlightsTable.js';
14+
import type { FlightLogsTable } from './types/connection/main/FlightLogsTable.js';
1415

1516
function createUTCDate(): Date {
1617
const now = new Date();
@@ -188,6 +189,122 @@ export async function getFlightsBySession(sessionId: string) {
188189
}
189190
}
190191

192+
export async function getFlightsByUser(userId: string) {
193+
try {
194+
const flights = await mainDb
195+
.selectFrom('flights')
196+
.selectAll()
197+
.where('user_id', '=', userId)
198+
.orderBy('created_at', 'desc')
199+
.execute();
200+
201+
return flights.map((flight) => sanitizeFlightForClient(flight));
202+
} catch (error) {
203+
console.error(`Error fetching flights for user ${userId}:`, error);
204+
return [];
205+
}
206+
}
207+
208+
export async function getFlightByIdForUser(userId: string, flightId: string) {
209+
try {
210+
const validFlightId = validateFlightId(flightId);
211+
const flight = await mainDb
212+
.selectFrom('flights')
213+
.selectAll()
214+
.where('id', '=', validFlightId)
215+
.where('user_id', '=', userId)
216+
.executeTakeFirst();
217+
218+
return flight ? sanitizeFlightForClient(flight) : null;
219+
} catch (error) {
220+
console.error(`Error fetching flight ${flightId} for user ${userId}:`, error);
221+
return null;
222+
}
223+
}
224+
225+
export async function getFlightLogsForUser(userId: string, flightId: string) {
226+
try {
227+
const validFlightId = validateFlightId(flightId);
228+
229+
const ownedFlight = await mainDb
230+
.selectFrom('flights')
231+
.select(['id', 'created_at'])
232+
.where('id', '=', validFlightId)
233+
.where('user_id', '=', userId)
234+
.executeTakeFirst();
235+
236+
if (!ownedFlight) {
237+
return { logs: [], logsDiscardedDueToAge: false };
238+
}
239+
240+
const retentionThreshold = new Date(
241+
Date.now() - 365 * 24 * 60 * 60 * 1000
242+
);
243+
const logsDiscardedDueToAge = !!(
244+
ownedFlight.created_at && ownedFlight.created_at < retentionThreshold
245+
);
246+
247+
const logs = await mainDb
248+
.selectFrom('flight_logs')
249+
.selectAll()
250+
.where('flight_id', '=', validFlightId)
251+
.orderBy('created_at', 'desc')
252+
.execute();
253+
254+
return {
255+
logs: logs.map((log: FlightLogsTable) => ({
256+
id: log.id,
257+
action: log.action,
258+
old_data: log.old_data,
259+
new_data: log.new_data,
260+
created_at: log.created_at,
261+
})),
262+
logsDiscardedDueToAge,
263+
};
264+
} catch (error) {
265+
console.error(
266+
`Error fetching flight logs for flight ${flightId} and user ${userId}:`,
267+
error
268+
);
269+
return { logs: [], logsDiscardedDueToAge: false };
270+
}
271+
}
272+
273+
export async function claimFlightForUser(
274+
sessionId: string,
275+
flightId: string,
276+
acarsToken: string,
277+
userId: string
278+
) {
279+
const validSessionId = validateSessionId(sessionId);
280+
const validFlightId = validateFlightId(flightId);
281+
282+
const flight = await mainDb
283+
.selectFrom('flights')
284+
.select(['id', 'user_id', 'acars_token'])
285+
.where('session_id', '=', validSessionId)
286+
.where('id', '=', validFlightId)
287+
.executeTakeFirst();
288+
289+
if (!flight) return { ok: false, reason: 'not_found' as const };
290+
if (flight.acars_token !== acarsToken)
291+
return { ok: false, reason: 'invalid_token' as const };
292+
if (flight.user_id && flight.user_id !== userId)
293+
return { ok: false, reason: 'already_claimed' as const };
294+
295+
await mainDb
296+
.updateTable('flights')
297+
.set({
298+
user_id: userId,
299+
updated_at: createUTCDate(),
300+
})
301+
.where('session_id', '=', validSessionId)
302+
.where('id', '=', validFlightId)
303+
.execute();
304+
305+
return { ok: true as const };
306+
}
307+
191308
export async function validateAcarsAccess(
192309
sessionId: string,
193310
flightId: string,
@@ -418,7 +535,7 @@ export async function updateFlight(
418535
for (const [key, value] of Object.entries(updates)) {
419536
let dbKey = key;
420537
if (key === 'cruisingFL') dbKey = 'cruisingfl';
421-
if (key === 'clearedFL') dbKey = 'clearedfl';
538+
if (key === 'clearedFL') dbKey = 'clearedfl';
422539
if (allowedColumns.includes(dbKey)) {
423540
dbUpdates[dbKey] = dbKey === 'clearance' ? String(value) : value;
424541
}

0 commit comments

Comments
 (0)