From 3bca2a86788512558888c39c4866f546fb05d96d Mon Sep 17 00:00:00 2001 From: AussieScorcher Date: Wed, 23 Jul 2025 18:33:45 +0800 Subject: [PATCH] Adds EuroScope file management API endpoints Implements comprehensive file management system for EuroScope with ICAO-based organization. Provides public file listing endpoint for retrieving files by airport code and authenticated endpoints for upload/delete operations. Includes access control integration with division system to ensure users can only manage files for airports they have permission to access. Enforces file size limits and restricts each ICAO to maximum 2 files to prevent storage abuse. --- src/index.ts | 213 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) diff --git a/src/index.ts b/src/index.ts index cc8faf9..f4fa42b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1524,6 +1524,219 @@ cdnApp.delete('/files/*', async (c) => { app.route('/cdn', cdnApp); +// EuroScope public file listing endpoint +app.get('/euroscope/files/:icao', async (c) => { + const icao = c.req.param('icao').toUpperCase(); + + // Validate ICAO format + if (!icao.match(/^[A-Z0-9]{4}$/)) { + return c.json({ + error: 'Invalid ICAO format. Must be exactly 4 uppercase letters/numbers.', + }, 400); + } + + try { + // Get list of files for this ICAO + const storage = ServicePool.getStorage(c.env); + const result = await storage.listFiles(`EuroScope/${icao}/`, 10); + + // Format for easier use by clients + const files = result.objects.map((obj) => ({ + fileName: obj.key.split('/').pop() || '', + size: obj.size, + uploaded: obj.uploaded.toISOString(), + url: new URL(`https://dev-cdn.stopbars.com/${obj.key}`, c.req.url).toString(), + })); + + return c.json({ + icao: icao, + files: files, + count: files.length, + }); + } catch (error) { + console.error('EuroScope public file listing error:', error); + return c.json({ + error: error instanceof Error ? error.message : 'Failed to list files', + }, 500); + } +}); + +// EuroScope file management endpoints +const euroscopeApp = new Hono<{ + Bindings: Env; + Variables: { + vatsimUser?: any; + user?: any; + }; +}>(); + +// Middleware for EuroScope endpoints to authenticate users +euroscopeApp.use('*', async (c, next) => { + const vatsimToken = c.req.header('X-Vatsim-Token'); + if (!vatsimToken) { + return c.text('Unauthorized', 401); + } + + const vatsim = ServicePool.getVatsim(c.env); + const auth = ServicePool.getAuth(c.env); + + const vatsimUser = await vatsim.getUser(vatsimToken); + const user = await auth.getUserByVatsimId(vatsimUser.id); + + if (!user) { + return c.text('User not found', 404); + } + + c.set('vatsimUser', vatsimUser); + c.set('user', user); + await next(); +}); + +// POST /euroscope/upload - Upload files to ICAO-specific folders +euroscopeApp.post('/upload', async (c) => { + const user = c.get('user'); + const vatsimUser = c.get('vatsimUser'); + + try { + const formData = await c.req.formData(); + const file = formData.get('file'); + const icao = formData.get('icao')?.toString()?.toUpperCase(); + + if (!file || !(file instanceof File)) { + return c.json({ + error: 'File is required', + }, 400); + } + + if (!icao) { + return c.json({ + error: 'ICAO code is required', + }, 400); + } + + // Validate ICAO format (exactly 4 uppercase letters/numbers) + if (!icao.match(/^[A-Z0-9]{4}$/)) { + return c.json({ + error: 'Invalid ICAO format. Must be exactly 4 uppercase letters/numbers.', + }, 400); + } + + // Check file size limit (10MB) + const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB in bytes + if (file.size > MAX_FILE_SIZE) { + return c.json({ + error: 'File size exceeds 10MB limit', + }, 400); + } + + // Check if user has access to upload files for this ICAO + const divisions = ServicePool.getDivisions(c.env); + const hasAccess = await divisions.userHasAirportAccess(vatsimUser.id.toString(), icao); + + if (!hasAccess) { + return c.json({ + error: 'You do not have permission to upload files for this airport. Please ensure your division has approved access to this ICAO.', + }, 403); + } + + // Create file path: EuroScope/ICAO/filename + const fileName = file.name; + const fileKey = `EuroScope/${icao}/${fileName}`; + + // Check if this would exceed the 2 files per ICAO limit + const storage = ServicePool.getStorage(c.env); + const existingFiles = await storage.listFiles(`EuroScope/${icao}/`, 10); + + // Count files that are not the one being replaced + const otherFiles = existingFiles.objects.filter(obj => obj.key !== fileKey); + if (otherFiles.length >= 2) { + return c.json({ + error: 'Maximum of 2 files per ICAO code allowed. Please delete an existing file before uploading a new one.', + }, 400); + } + + // Extract file data + const fileData = await file.arrayBuffer(); + + // Upload file to storage with metadata + const result = await storage.uploadFile(fileKey, fileData, file.type, { + uploadedBy: vatsimUser.id.toString(), + icao: icao, + fileName: file.name, + size: file.size.toString(), + }); + + + // Return success with download URL + return c.json({ + success: true, + file: { + key: result.key, + icao: icao, + fileName: fileName, + size: file.size, + url: new URL(`https://dev-cdn.stopbars.com/${result.key}`, c.req.url).toString(), + }, + }, 201); + } catch (error) { + console.error('EuroScope file upload error:', error); + return c.json({ + error: error instanceof Error ? error.message : 'Failed to upload file', + }, 500); + } +}); + +// DELETE /euroscope/files/:icao/:filename - Delete a specific file +euroscopeApp.delete('/files/:icao/:filename', async (c) => { + const icao = c.req.param('icao').toUpperCase(); + const filename = c.req.param('filename'); + const user = c.get('user'); + const vatsimUser = c.get('vatsimUser'); + + // Validate ICAO format + if (!icao.match(/^[A-Z0-9]{4}$/)) { + return c.json({ + error: 'Invalid ICAO format. Must be exactly 4 uppercase letters/numbers.', + }, 400); + } + + try { + // Check if user has access to delete files for this ICAO + const divisions = ServicePool.getDivisions(c.env); + const hasAccess = await divisions.userHasAirportAccess(vatsimUser.id.toString(), icao); + + if (!hasAccess) { + return c.json({ + error: 'You do not have permission to delete files for this airport. Please ensure your division has approved access to this ICAO.', + }, 403); + } + + // Construct the file key + const fileKey = `EuroScope/${icao}/${filename}`; + + // Delete the file + const storage = ServicePool.getStorage(c.env); + const deleted = await storage.deleteFile(fileKey); + + if (!deleted) { + return c.json({ + error: 'File not found', + }, 404); + } + + return c.json({ + success: true, + message: `File ${filename} deleted successfully from ${icao}`, + }); + } catch (error) { + console.error('EuroScope file deletion error:', error); + return c.json({ + error: error instanceof Error ? error.message : 'Failed to delete file', + }, 500); + } +}); +app.route('/euroscope', euroscopeApp); + app.post('/purge-cache', async (c) => { const vatsimToken = c.req.header('X-Vatsim-Token'); if (!vatsimToken) {