diff --git a/bun.lock b/bun.lock index 7cb5ba2..004eee6 100644 --- a/bun.lock +++ b/bun.lock @@ -5,13 +5,13 @@ "name": "bars-core", "dependencies": { "geolib": "^3.3.4", - "hono": "^4.10.4", + "hono": "^4.10.5", "nanoid": "^5.1.6", "swagger-jsdoc": "^6.2.8", }, "devDependencies": { "@eslint/js": "^9.39.1", - "@types/node": "^24.10.0", + "@types/node": "^24.10.1", "@types/swagger-jsdoc": "^6.0.4", "eslint": "^9.39.1", "globals": "^16.5.0", @@ -19,8 +19,8 @@ "prettier": "^3.6.2", "ts-node": "^10.9.2", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.3", - "wrangler": "^4.46.0", + "typescript-eslint": "^8.46.4", + "wrangler": "^4.48.0", }, }, }, diff --git a/schema.sql b/schema.sql index 0f5756a..744ad4d 100644 --- a/schema.sql +++ b/schema.sql @@ -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 ( diff --git a/src/index.ts b/src/index.ts index 1bf99d1..654dfed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 }; + 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 diff --git a/src/services/airport.ts b/src/services/airport.ts index e488afa..7bd9165 100644 --- a/src/services/airport.ts +++ b/src/services/airport.ts @@ -9,6 +9,10 @@ interface AirportData { name?: string; continent?: string; elevation_ft?: string; + country?: { + code: string; + name: string; + }; runways?: Array<{ length_ft: string; width_ft: string; @@ -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]; @@ -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]); } @@ -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, @@ -310,6 +320,8 @@ export class AirportService { airport.continent, airport.elevation_ft, airport.elevation_m, + airport.country_code, + airport.country_name, ], ); @@ -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] }; diff --git a/src/services/divisions.ts b/src/services/divisions.ts index 25a36d1..6e1e539 100644 --- a/src/services/divisions.ts +++ b/src/services/divisions.ts @@ -232,6 +232,33 @@ export class DivisionService { })); } + async deleteAirportRequest(divisionId: number, icao: string, vatsimId: string): Promise { + // 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()], + ); + + 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;