Skip to content
Closed
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
8 changes: 4 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ CREATE TABLE IF NOT EXISTS airports (
bbox_min_lat REAL,
bbox_min_lon REAL,
bbox_max_lat REAL,
bbox_max_lon REAL
bbox_max_lon REAL,
country_code TEXT,
country_name TEXT
);

CREATE TABLE IF NOT EXISTS runways (
Expand Down
58 changes: 58 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2153,6 +2153,64 @@ divisionsApp.post('/:id/airports/:airportId/approve', async (c) => {
return c.json(airport);
});

// DELETE /divisions/:id/airports - Delete airport request (only pending or rejected, by any division member)
/**
* @openapi
* /divisions/{id}/airports:
* delete:
* x-hidden: true
* summary: Delete airport request
* description: Allows any division member to delete an airport request if the status is pending or rejected
* tags:
* - Divisions
* security:
* - VatsimToken: []
* parameters:
* - in: path
* name: id
* required: true
* schema: { type: integer }
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [icao]
* properties:
* icao:
* type: string
* responses:
* 200:
* description: Airport request deleted
* 403:
* description: Forbidden - not a division member or request is approved
* 404:
* description: Division or airport request not found
*/
divisionsApp.delete('/:id/airports', async (c) => {
const vatsimToken = c.req.header('X-Vatsim-Token');
if (!vatsimToken) return c.text('Unauthorized', 401);

const divisionId = parseInt(c.req.param('id'));
const { icao } = (await c.req.json()) as { icao: string };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation for the icao field. If the request body doesn't include icao or if it's null/undefined, the code will crash when deleteAirportRequest tries to call icao.toUpperCase() on line 245 of divisions.ts.

Add validation after parsing the JSON:

Suggested change
const { icao } = (await c.req.json()) as { icao: string };
const { icao } = (await c.req.json()) as { icao: string };
if (!icao) {
return c.text('ICAO code is required', 400);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/index.ts
Line: 2196:2196

Comment:
Missing input validation for the `icao` field. If the request body doesn't include `icao` or if it's `null`/`undefined`, the code will crash when `deleteAirportRequest` tries to call `icao.toUpperCase()` on line 245 of divisions.ts.

Add validation after parsing the JSON:

```suggestion
	const { icao } = (await c.req.json()) as { icao: string };
	if (!icao) {
		return c.text('ICAO code is required', 400);
	}
```

How can I resolve this? If you propose a fix, please make it concise.

const vatsim = ServicePool.getVatsim(c.env);
const divisions = ServicePool.getDivisions(c.env);

// Verify division exists
const division = await divisions.getDivision(divisionId);
if (!division) {
return c.text('Division not found', 404);
}

const vatsimUser = await vatsim.getUser(vatsimToken);
const deleted = await divisions.deleteAirportRequest(divisionId, icao, vatsimUser.id);
if (!deleted) {
return c.text('Airport request not found or cannot be deleted', 404);
}
return c.json({ success: true });
});

app.route('/divisions', divisionsApp);

// Points endpoints
Expand Down
16 changes: 15 additions & 1 deletion src/services/airport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ interface AirportData {
name?: string;
continent?: string;
elevation_ft?: string;
country?: {
code: string;
name: string;
};
Comment on lines +12 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AirportData interface defines country as a nested object with code and name properties, but the database stores these as separate country_code and country_name columns. When airports are returned from the database (lines 286, 419, 430), they contain the flat structure (country_code, country_name) rather than the nested structure defined in this interface.

This creates a type mismatch between the interface definition and the actual runtime data structure. Consider either:

  1. Transforming the database result to match the interface (add a country object when country_code exists)
  2. Or updating the interface to reflect the actual flat structure returned by getAirport()
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/services/airport.ts
Line: 12:15

Comment:
The `AirportData` interface defines `country` as a nested object with `code` and `name` properties, but the database stores these as separate `country_code` and `country_name` columns. When airports are returned from the database (lines 286, 419, 430), they contain the flat structure (`country_code`, `country_name`) rather than the nested structure defined in this interface.

This creates a type mismatch between the interface definition and the actual runtime data structure. Consider either:
1. Transforming the database result to match the interface (add a `country` object when `country_code` exists)
2. Or updating the interface to reflect the actual flat structure returned by `getAirport()`

How can I resolve this? If you propose a fix, please make it concise.

runways?: Array<{
length_ft: string;
width_ft: string;
Expand Down Expand Up @@ -194,6 +198,8 @@ export class AirportService {
bbox_min_lon: number | null;
bbox_max_lat: number | null;
bbox_max_lon: number | null;
country_code: string | null;
country_name: string | null;
}>('SELECT * FROM airports WHERE icao = ?', [uppercaseIcao]);
const airportFromDb = airportResult.results[0];

Expand Down Expand Up @@ -250,6 +256,8 @@ export class AirportService {
bbox_min_lon: number | null;
bbox_max_lat: number | null;
bbox_max_lon: number | null;
country_code: string | null;
country_name: string | null;
}>('SELECT * FROM airports WHERE icao = ?', [uppercaseIcao]);
if (reread.results[0]) Object.assign(airportFromDb, reread.results[0]);
}
Expand Down Expand Up @@ -297,11 +305,13 @@ export class AirportService {
continent: airportData.continent || 'UNKNOWN',
elevation_ft: !Number.isNaN(elevation_ft) ? elevation_ft : null,
elevation_m,
country_code: airportData.country?.code || null,
country_name: airportData.country?.name || null,
};

// Save airport to database using write-optimized operation
await this.dbSession.executeWrite(
'INSERT INTO airports (icao, latitude, longitude, name, continent, elevation_ft, elevation_m) VALUES (?, ?, ?, ?, ?, ?, ?)',
'INSERT INTO airports (icao, latitude, longitude, name, continent, elevation_ft, elevation_m, country_code, country_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
airport.icao,
airport.latitude ?? null,
Expand All @@ -310,6 +320,8 @@ export class AirportService {
airport.continent,
airport.elevation_ft,
airport.elevation_m,
airport.country_code,
airport.country_name,
],
);

Expand Down Expand Up @@ -341,6 +353,8 @@ export class AirportService {
bbox_min_lon: number | null;
bbox_max_lat: number | null;
bbox_max_lon: number | null;
country_code: string | null;
country_name: string | null;
}>('SELECT * FROM airports WHERE icao = ?', [uppercaseIcao]);
const mergedAirport = { ...airport, ...reread.results[0] };

Expand Down
27 changes: 27 additions & 0 deletions src/services/divisions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,33 @@ export class DivisionService {
}));
}

async deleteAirportRequest(divisionId: number, icao: string, vatsimId: string): Promise<boolean> {
// Verify user is a member of this division
const role = await this.getMemberRole(divisionId, vatsimId);
if (!role) throw new HttpError(403, 'Forbidden: User is not a member of this division');

// Delete only if the division has requested this ICAO and status is pending or rejected
const result = await this.dbSession.executeWrite(
`DELETE FROM division_airports
WHERE division_id = ? AND icao = ? AND status IN ('pending', 'rejected')
RETURNING id`,
[divisionId, icao.toUpperCase()],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code will crash with a runtime error if icao is undefined or null when calling toUpperCase(). This can happen if the API endpoint receives a JSON body without the icao field or with icao: null.

Add validation before using the parameter:

Suggested change
[divisionId, icao.toUpperCase()],
if (!icao) throw new HttpError(400, 'ICAO code is required');
// Delete only if the division has requested this ICAO and status is pending or rejected
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/services/divisions.ts
Line: 245:245

Comment:
The code will crash with a runtime error if `icao` is `undefined` or `null` when calling `toUpperCase()`. This can happen if the API endpoint receives a JSON body without the `icao` field or with `icao: null`.

Add validation before using the parameter:

```suggestion
		if (!icao) throw new HttpError(400, 'ICAO code is required');
		
		// Delete only if the division has requested this ICAO and status is pending or rejected
```

How can I resolve this? If you propose a fix, please make it concise.

);

const rows = result.results as unknown as Array<{ id: number }> | null;
const deleted = !!(rows && rows[0]);

if (deleted) {
try {
this.posthog?.track('Division Airport Request Deleted', { divisionId, icao, deletedBy: vatsimId });
} catch (e) {
console.warn('Posthog track failed (Division Airport Request Deleted)', e);
}
}

return deleted;
}

async getDivisionMembers(divisionId: number): Promise<(DivisionMember & { display_name: string })[] | null> {
type DivisionMemberRow = {
division_exists: number | null;
Expand Down