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
113 changes: 48 additions & 65 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,73 +1,56 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PFControl v2</title>
<meta
name="description"
content="PFControl is the next-generation flight strip platform for real-time coordination between air traffic controllers. Secure, reliable, and collaborative."
/>

<meta
name="keywords"
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"
/>
<meta name="author" content="PFConnect Studios" />
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PFControl v2</title>
<meta name="description"
content="PFControl is the next-generation flight strip platform for real-time coordination between air traffic controllers. Secure, reliable, and collaborative." />

<meta
property="og:title"
content="PFControl v2 - Flight Strips for Project Flight & Roblox Aviation"
/>
<meta
property="og:description"
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."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://pfcontrol.com/" />
<meta name="keywords"
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" />
<meta name="author" content="Cephie Studios" />

<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="PFControl v2 - Flight Strips for Project Flight & Roblox Aviation"
/>
<meta
name="twitter:description"
content="Professional flight strip platform for Project Flight, PTFS, and other Roblox aviation games. Real-time ATC coordination made easy."
/>
<meta property="og:title" content="PFControl v2 - Flight Strips for Project Flight & Roblox Aviation" />
<meta property="og:description"
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." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://pfcontrol.com/" />

<meta name="robots" content="index, follow" />
<meta name="googlebot" content="index, follow" />
<meta name="google-adsense-account" content="ca-pub-3075420086521736">
<link rel="canonical" href="https://pfcontrol.com/" />
<link
rel="sitemap"
type="application/xml"
title="Sitemap"
href="/sitemap.xml"
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="PFControl v2 - Flight Strips for Project Flight & Roblox Aviation" />
<meta name="twitter:description"
content="Professional flight strip platform for Project Flight, PTFS, and other Roblox aviation games. Real-time ATC coordination made easy." />

<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "PFControl v2",
"description": "Flight strip platform for Project Flight and Roblox aviation games",
"url": "https://pfcontrol.com/",
"applicationCategory": "Gaming, Aviation Simulation",
"operatingSystem": "Web Browser",
"keywords": "Project Flight, PTFS, Roblox aviation, flight strips, ATC",
"author": {
"@type": "Organization",
"name": "PFConnect Studios"
}
<meta name="robots" content="index, follow" />
<meta name="googlebot" content="index, follow" />
<meta name="google-adsense-account" content="ca-pub-3075420086521736">
<link rel="canonical" href="https://pfcontrol.com/" />
<link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml" />

<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "PFControl v2",
"description": "Flight strip platform for Project Flight and Roblox aviation games",
"url": "https://pfcontrol.com/",
"applicationCategory": "Gaming, Aviation Simulation",
"operatingSystem": "Web Browser",
"keywords": "Project Flight, PTFS, Roblox aviation, flight strips, ATC",
"author": {
"@type": "Organization",
"name": "Cephie Studios"
}
</script>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3075420086521736" crossorigin="anonymous"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
}
</script>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

</html>
22 changes: 10 additions & 12 deletions server/db/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,13 @@
let decryptedIP = null;
if (log.ip_address) {
try {
const parsed = JSON.parse(log.ip_address as string);
const parsed =
typeof log.ip_address === 'string'
? JSON.parse(log.ip_address)
: log.ip_address;
decryptedIP = decrypt(parsed);
} catch {
try {
decryptedIP = decrypt(JSON.parse(log.ip_address as string));
} catch {
decryptedIP = null;
}
decryptedIP = null;
}
}
return {
Expand Down Expand Up @@ -231,14 +230,13 @@
let decryptedIP = null;
if (result.ip_address) {
try {
const parsed = JSON.parse(result.ip_address as string);
const parsed =
typeof result.ip_address === 'string'
? JSON.parse(result.ip_address)
: result.ip_address;
decryptedIP = decrypt(parsed);
} catch {
try {
decryptedIP = decrypt(JSON.parse(result.ip_address as string));
} catch {
decryptedIP = null;
}
decryptedIP = null;
}
}

Expand Down Expand Up @@ -274,7 +272,7 @@
}
}

let cleanupInterval: NodeJS.Timeout | null = null;

Check warning on line 275 in server/db/audit.ts

View workflow job for this annotation

GitHub Actions / build

'cleanupInterval' is assigned a value but never used

Check warning on line 275 in server/db/audit.ts

View workflow job for this annotation

GitHub Actions / build

'cleanupInterval' is assigned a value but never used

function startAutomaticCleanup() {
const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000;
Expand Down
26 changes: 21 additions & 5 deletions server/db/flightLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { mainDb } from './connection.js';
import { encrypt, decrypt } from '../utils/encryption.js';
import { sql } from 'kysely';

const FLIGHT_LOG_RETENTION_DAYS = 365;

export interface FlightLogData {
userId: string;
username: string;
Expand Down Expand Up @@ -60,7 +62,9 @@ export async function logFlightAction(logData: FlightLogData) {
}
}

export async function cleanupOldFlightLogs(daysToKeep = 30) {
export async function cleanupOldFlightLogs(
daysToKeep = FLIGHT_LOG_RETENTION_DAYS
) {
try {
const result = await mainDb
.deleteFrom('flight_logs')
Expand All @@ -85,15 +89,15 @@ export function startFlightLogsCleanup() {

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

cleanupInterval = setInterval(async () => {
try {
await cleanupOldFlightLogs(30);
await cleanupOldFlightLogs(FLIGHT_LOG_RETENTION_DAYS);
} catch (error) {
console.error('Scheduled flight logs cleanup failed:', error);
}
Expand Down Expand Up @@ -229,7 +233,13 @@ export async function getFlightLogs(
return {
logs: logs.map((log) => ({
...log,
ip_address: log.ip_address ? decrypt(JSON.parse(log.ip_address)) : null,
ip_address: log.ip_address
? decrypt(
typeof log.ip_address === 'string'
? JSON.parse(log.ip_address)
: log.ip_address
)
: null,
})),
pagination: {
page,
Expand All @@ -256,7 +266,13 @@ export async function getFlightLogById(logId: string | number) {

return {
...log,
ip_address: log.ip_address ? decrypt(JSON.parse(log.ip_address)) : null,
ip_address: log.ip_address
? decrypt(
typeof log.ip_address === 'string'
? JSON.parse(log.ip_address)
: log.ip_address
)
: null,
};
} catch (error) {
console.error('Error fetching flight log by ID:', error);
Expand Down
119 changes: 118 additions & 1 deletion server/db/flights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { sql } from 'kysely';
import { incrementStat } from '../utils/statisticsCache.js';
import type { FlightsTable } from './types/connection/main/FlightsTable.js';
import type { FlightLogsTable } from './types/connection/main/FlightLogsTable.js';

function createUTCDate(): Date {
const now = new Date();
Expand Down Expand Up @@ -64,9 +65,9 @@

function sanitizeFlightForClient(flight: FlightsTable): ClientFlight {
const {
user_id: _uid,

Check warning on line 68 in server/db/flights.ts

View workflow job for this annotation

GitHub Actions / build

'_uid' is assigned a value but never used

Check warning on line 68 in server/db/flights.ts

View workflow job for this annotation

GitHub Actions / build

'_uid' is assigned a value but never used
ip_address: _ip,

Check warning on line 69 in server/db/flights.ts

View workflow job for this annotation

GitHub Actions / build

'_ip' is assigned a value but never used

Check warning on line 69 in server/db/flights.ts

View workflow job for this annotation

GitHub Actions / build

'_ip' is assigned a value but never used
acars_token: _tok,

Check warning on line 70 in server/db/flights.ts

View workflow job for this annotation

GitHub Actions / build

'_tok' is assigned a value but never used

Check warning on line 70 in server/db/flights.ts

View workflow job for this annotation

GitHub Actions / build

'_tok' is assigned a value but never used
cruisingfl,
clearedfl,
...rest
Expand All @@ -81,7 +82,7 @@
function sanitizeFlightForOwner(
flight: FlightsTable
): ClientFlight & { acars_token?: string } {
const { user_id: _uid, ip_address: _ip, cruisingfl, clearedfl, ...rest } =

Check warning on line 85 in server/db/flights.ts

View workflow job for this annotation

GitHub Actions / build

'_uid' is assigned a value but never used

Check warning on line 85 in server/db/flights.ts

View workflow job for this annotation

GitHub Actions / build

'_uid' is assigned a value but never used
flight;
return {
...rest,
Expand Down Expand Up @@ -188,6 +189,122 @@
}
}

export async function getFlightsByUser(userId: string) {
try {
const flights = await mainDb
.selectFrom('flights')
.selectAll()
.where('user_id', '=', userId)
.orderBy('created_at', 'desc')
.execute();

return flights.map((flight) => sanitizeFlightForClient(flight));
} catch (error) {
console.error(`Error fetching flights for user ${userId}:`, error);
return [];
}
}

export async function getFlightByIdForUser(userId: string, flightId: string) {
try {
const validFlightId = validateFlightId(flightId);
const flight = await mainDb
.selectFrom('flights')
.selectAll()
.where('id', '=', validFlightId)
.where('user_id', '=', userId)
.executeTakeFirst();

return flight ? sanitizeFlightForClient(flight) : null;
} catch (error) {
console.error(`Error fetching flight ${flightId} for user ${userId}:`, error);
return null;
}
}

export async function getFlightLogsForUser(userId: string, flightId: string) {
try {
const validFlightId = validateFlightId(flightId);

const ownedFlight = await mainDb
.selectFrom('flights')
.select(['id', 'created_at'])
.where('id', '=', validFlightId)
.where('user_id', '=', userId)
.executeTakeFirst();

if (!ownedFlight) {
return { logs: [], logsDiscardedDueToAge: false };
}

const retentionThreshold = new Date(
Date.now() - 365 * 24 * 60 * 60 * 1000
);
const logsDiscardedDueToAge = !!(
ownedFlight.created_at && ownedFlight.created_at < retentionThreshold
);

const logs = await mainDb
.selectFrom('flight_logs')
.selectAll()
.where('flight_id', '=', validFlightId)
.orderBy('created_at', 'desc')
.execute();

return {
logs: logs.map((log: FlightLogsTable) => ({
id: log.id,
action: log.action,
old_data: log.old_data,
new_data: log.new_data,
created_at: log.created_at,
})),
logsDiscardedDueToAge,
};
} catch (error) {
console.error(
`Error fetching flight logs for flight ${flightId} and user ${userId}:`,
error
);
return { logs: [], logsDiscardedDueToAge: false };
}
}

export async function claimFlightForUser(
sessionId: string,
flightId: string,
acarsToken: string,
userId: string
) {
const validSessionId = validateSessionId(sessionId);
const validFlightId = validateFlightId(flightId);

const flight = await mainDb
.selectFrom('flights')
.select(['id', 'user_id', 'acars_token'])
.where('session_id', '=', validSessionId)
.where('id', '=', validFlightId)
.executeTakeFirst();

if (!flight) return { ok: false, reason: 'not_found' as const };
if (flight.acars_token !== acarsToken)
return { ok: false, reason: 'invalid_token' as const };
if (flight.user_id && flight.user_id !== userId)
return { ok: false, reason: 'already_claimed' as const };

await mainDb
.updateTable('flights')
.set({
user_id: userId,
updated_at: createUTCDate(),
})
.where('session_id', '=', validSessionId)
.where('id', '=', validFlightId)
.execute();

return { ok: true as const };
}

export async function validateAcarsAccess(
sessionId: string,
flightId: string,
Expand Down Expand Up @@ -418,7 +535,7 @@
for (const [key, value] of Object.entries(updates)) {
let dbKey = key;
if (key === 'cruisingFL') dbKey = 'cruisingfl';
if (key === 'clearedFL') dbKey = 'clearedfl';
if (key === 'clearedFL') dbKey = 'clearedfl';
if (allowedColumns.includes(dbKey)) {
dbUpdates[dbKey] = dbKey === 'clearance' ? String(value) : value;
}
Expand Down
Loading
Loading