From 1b5c5606d701fc11441699c7a060788baaafd184 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Sat, 28 Jun 2025 22:34:36 -0700 Subject: [PATCH 01/24] fix: Updated docker-compose files for production and development environments --- docker-compose-prod.yaml | 4 ++-- docker-compose.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose-prod.yaml b/docker-compose-prod.yaml index 4ba00ed..641dd38 100644 --- a/docker-compose-prod.yaml +++ b/docker-compose-prod.yaml @@ -2,7 +2,7 @@ services: vis: container_name: vis - image: ghcr.io/local-connectivity-lab/ccn-coverage-vis:latest + image: ccn-coverage-vis:latest ports: - "8090:80" depends_on: @@ -12,7 +12,7 @@ services: api: container_name: api - image: ghcr.io/local-connectivity-lab/ccn-coverage-api:latest + image: ccn-coverage-api:latest ports: - "8091:3000" environment: diff --git a/docker-compose.yaml b/docker-compose.yaml index 473518f..a8486f1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,7 +8,7 @@ services: - MONGODB_URI=mongodb://mongodb:27017/api-data - LDAP_URI=ldap://ldap:389 volumes: - - ./keys/:/app/keys + - ./keys/:/usr/src/app/keys/ depends_on: - mongodb - ldap From fea431abb55c5d6fbe0bc4775df8435c35db3594 Mon Sep 17 00:00:00 2001 From: Ananya Date: Sat, 12 Jul 2025 00:31:54 -0700 Subject: [PATCH 02/24] feat(api): add secure site management and public sites endpoints Add two new API endpoints for site management: - /api/secure-site with POST, PUT, and DELETE operations for authenticated site management - /api/public-sites with GET operation to retrieve public sites list Regenerated schema.d.ts using make build-typescript to include TypeScript definitions for the new endpoints and their request/response types. --- src/types/schema.d.ts | 3656 ++++++++++++++++++++++------------------- 1 file changed, 1950 insertions(+), 1706 deletions(-) diff --git a/src/types/schema.d.ts b/src/types/schema.d.ts index 07f96f8..d4267db 100644 --- a/src/types/schema.d.ts +++ b/src/types/schema.d.ts @@ -4,1736 +4,1980 @@ */ export interface paths { - '/api/register': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Registers a new user */ - post: operations['registerUser']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/report_signal': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Report a signal strength measurement. */ - post: operations['reportSignal']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/report_measurement': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Report a speed test measurement. */ - post: operations['reportMeasurement']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/data': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Retrieve network data - * @description Fetches network data with optional filtering by cell_id or timestamp range, and visual display parameters. When filtering by timestamp, both timestamp_from and timestamp_to must be provided together to define a date range. Results are always sorted by timestamp. - */ - get: { - parameters: { - query?: { - /** @description Filter results by cell identifier */ - cell_id?: string; - /** @description Start of timestamp range - must be used together with timestamp_to */ - timestamp_from?: string; - /** @description End of timestamp range - must be used together with timestamp_from */ - timestamp_to?: string; - /** @description Width of the display area in pixels */ - width?: number; - /** @description Height of the display area in pixels */ - height?: number; - /** @description Top coordinate of the viewport */ - top?: number; - /** @description Left coordinate of the viewport */ - left?: number; - /** @description Controls the size of data bins for aggregation */ - binSizeShift?: number; - /** @description Zoom level for the map view */ - zoom?: number; - /** @description Comma-separated list of selected site identifiers */ - selectedSites?: string; - /** @description Type of map visualization to display */ - mapType?: string; - /** @description Alternative format for timestamp_from */ - timeFrom?: string; - /** @description Alternative format for timestamp_to */ - timeTo?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description A list of network data records, sorted by timestamp */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['QueryData'][]; - }; - }; - /** @description Invalid input */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/sucess': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Success response - * @description Returns a success message, typically used as a redirect target after successful authentication - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/failure': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Failure response - * @description Returns an error message, typically used as a redirect target after failed authentication - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Authorization failure */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/sitesSummary': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get summary metrics for all sites - * @description Returns average ping, download speed, upload speed, and signal strength for each site within the specified time range - */ - get: { - parameters: { - query: { - /** @description Start of the time range */ - timeFrom: string; - /** @description End of the time range */ - timeTo: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Summary metrics for all sites */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SitesSummary']; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/lineSummary': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get time series data for selected sites - * @description Returns time series data for the specified metric and sites within the specified time range - */ - get: { - parameters: { - query: { - /** @description Type of metric to aggregate (ping, download_speed, upload_speed, or dbm) */ - mapType: string; - /** @description Comma-separated list of site names to include */ - selectedSites: string; - /** @description Start of the time range */ - timeFrom: string; - /** @description End of the time range */ - timeTo: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Time series data for selected sites */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['LineSummaryItem'][]; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/markers': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get marker data for map visualization - * @description Returns geolocation data with network metrics for selected sites and devices within the specified time range - */ - get: { - parameters: { - query: { - /** @description Comma-separated list of site names to include */ - sites: string; - /** @description Comma-separated list of device IDs to include */ - devices?: string; - /** @description Start of the time range */ - timeFrom: string; - /** @description End of the time range */ - timeTo: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Marker data for map visualization */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['MarkerData'][]; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/dataRange': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get geographic boundaries of available data - * @description Returns the center coordinates and bounding box (minimum and maximum latitude/longitude) of all available measurement data. - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Geographic boundaries of available data */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['DataRangeResponse']; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/sites': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get all sites - * @description Returns a list of all available sites with their location and status information - */ - get: operations['getSites']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/secure/get_groups': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Get distinct group identifiers - * @description Returns a list of unique group identifiers across signal and measurement data - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully retrieved group list */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GroupList']; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/secure/delete_group': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Delete a group of measurements - * @description Removes all measurements associated with the specified group - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['DeleteGroupRequest']; - }; - }; - responses: { - /** @description Successfully deleted group */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/secure/delete_manual': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Delete manual measurements - * @description Removes all manually entered measurements from the database - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully deleted manual measurements */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/secure/upload_data': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Upload and process measurement data - * @description Parses CSV data and stores it as both signal and measurement records. - * If a group is specified, any existing data with that group will be removed first. - * The CSV should include columns for date, time, coordinate, cell_id, dbm, ping, download_speed, and upload_speed. - * - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['UploadDataRequest']; - }; - }; - responses: { - /** @description Successfully uploaded and processed data */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Bad request - missing CSV data or incorrect format */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server error during processing */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Service unavailable - database operation failed */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/secure/get-users': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Get lists of registered and pending users - * @description Returns two lists: - * 1. Registered users sorted by issue date (newest first) - * 2. Pending users whose issue date is within the expiry display limit, sorted by issue date (newest first) - * - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully retrieved user lists */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetUserResponse']; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/secure/toggle-users': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Toggle a user's enabled status - * @description Enables or disables a user account by their identity - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['ToggleUserRequest']; - }; - }; - responses: { - /** @description Successfully toggled user status */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Bad request - missing or invalid parameters */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Service unavailable - database operation failed */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/secure/login': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * LDAP authentication - * @description Authenticates a user against an LDAP directory server. - * Uses Passport LDAP strategy which binds to the LDAP server with the provided credentials. - * On success, creates a session and redirects to /api/success. - * On failure, redirects to /api/failure. - * - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['LdapCredentials']; - 'application/x-www-form-urlencoded': components['schemas']['LdapCredentials']; - }; - }; - responses: { - /** @description Successful authentication */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example success */ - result: string; - }; - }; - }; - /** @description Authentication failed */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example Invalid credentials */ - error: string; - }; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @example Failed to establish session */ - error: string; - }; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/logout': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Log out user - * @description Ends the user's authenticated session - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully logged out */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/secure/edit_sites': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Update site configuration - * @description Updates the sites configuration file with provided data. - * Requires user to be authenticated - will redirect to login page if not authenticated. - * - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/x-www-form-urlencoded': { - /** - * @description JSON string containing site configuration data - * @example {"site1":{"name":"Site 1","location":"Building A"},"site2":{"name":"Site 2","location":"Building B"}} - */ - sites: string; - }; - }; - }; - responses: { - /** @description Site configuration successfully updated */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Bad request - invalid JSON format or missing required fields */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Unauthorized - User not logged in */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Server error while updating configuration file */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/secure/new-user': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Create a new user with cryptographic identity - * @description Creates a new user with a cryptographically secure identity using EC keys. - * Generates keypairs, creates signatures, and stores user information. - * Requires authentication - will redirect to login page if not authenticated. - * - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['NewUserRequest']; - }; - }; - responses: { - /** @description User successfully created with cryptographic identity */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['NewUserRequest']; - }; - }; - /** @description Unauthorized - User not logged in */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Cryptographic operation error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Database operation error */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; -} -export type webhooks = Record; -export interface components { - schemas: { - NewUserRequest: { - /** - * Format: email - * @description User's email address - * @example user@example.com - */ - email?: string; - /** - * @description User's first name - * @example John - */ - firstName?: string; - /** - * @description User's last name - * @example Doe - */ - lastName?: string; - }; - LdapCredentials: { - /** - * @description LDAP username (could be DN, uid, or email depending on LDAP configuration) - * @example uid=jsmith,ou=users,dc=example,dc=com - */ - username: string; - /** - * Format: password - * @description LDAP password - * @example password123 - */ - password: string; - }; - CryptoIdentityResponse: { - /** - * @description Cryptographic signature in hexadecimal format - * @example 30450221009d41a9afd... - */ - sigma_t?: string; - /** - * @description Private key in DER format converted to hexadecimal - * @example 308184020100301006... - */ - sk_t?: string; - /** - * @description Public key in DER format converted to hexadecimal - * @example 3056301006072a8648... - */ - pk_a?: string; - }; - UploadDataRequest: { - /** - * @description CSV data to be parsed and stored - * @example date,time,coordinate,cell_id,dbm,ping,download_speed,upload_speed - * 2021-01-25,18:43:54,47.681932,-122.318292,cell-1,-85.3,-87.1,137.4,5.2,7.3 - */ - csv: string; - /** - * @description Optional group identifier to associate with uploaded data - * @example fieldtrip-2021 - */ - group?: string; - }; - DeleteGroupRequest: { - /** - * @description Group identifier to delete - * @example fieldtrip-2021 - */ - group: string; - }; - /** @description List of unique group identifiers across signal and measurement data */ - GroupList: string[]; - User: { - /** - * @description Unique identifier for the user - * @example 9a8b7c6d5e4f3g2h1i - */ - identity: string; - /** - * Format: email - * @description User's email address - * @example user@example.com - */ - email: string; - /** - * @description User's first name - * @example John - */ - firstName: string; - /** - * @description User's last name - * @example Doe - */ - lastName: string; - /** - * @description Whether the user has completed registration - * @example true - */ - registered: boolean; - /** - * Format: date-time - * @description Date when the user was issued or account was created - * @example 2023-03-15T14:30:45.123Z - */ - issueDate: string; - /** - * @description Whether the user account is currently enabled - * @example true - */ - isEnabled: boolean; - /** - * @description User's public key - * @example 308201a2300d06092a864886f70d01010105000382018f003082018a02820181... - */ - publicKey: string; - /** - * @description QR code data for user registration - * @example {"sigma_t":"...","sk_t":"...","pk_a":"..."} - */ - qrCode: string; - /** - * Format: date-time - * @description Last time the user was online in ISO 8601 date-time (YYYY-MM-DDTHH:mm:ss.sssZ) - * @example 2023-03-15T14:30:45.123Z - */ - lastOnline: string; - }; - GetUserResponse: { - /** @description List of pending users (not yet registered) whose issue date is within the expiry limit */ - pending: components['schemas']['User'][]; - /** @description List of registered users */ - registered: components['schemas']['User'][]; - }; - ToggleUserRequest: { - /** - * @description Unique identifier of the user to update - * @example 9a8b7c6d5e4f3g2h1i - */ - identity: string; - /** - * @description New enabled status for the user - * @example true - */ - enabled: boolean; - }; - BaseMeasureDataModel: { - /** - * Format: double - * @description Geographic latitude coordinate - */ - latitude: number; - /** - * Format: double - * @description Geographic longitude coordinate - */ - longitude: number; - /** @description When the measurement was taken */ - timestamp: string; - /** @description Identifier for the cell tower or access point */ - cell_id: string; - /** @description Unique identifier for the reporting device */ - device_id: string; - /** @description Flag indicating if the measurement has been reported and will be shown on the map. */ - show_data: boolean; - }; - SignalStrengthReportModel: components['schemas']['BaseMeasureDataModel'] & { - /** @description Signal strength in decibel-milliwatts */ - dbm: number; - /** @description Code representing the signal strength level */ - level_code: number; - }; - ConnectivityReportModel: components['schemas']['BaseMeasureDataModel'] & { - /** - * Format: double - * @description Upload speed measurement - */ - upload_speed: number; - /** - * Format: double - * @description Download speed measurement - */ - download_speed: number; - /** - * Format: double - * @description Network latency in milliseconds - */ - ping: number; - /** - * Format: double - * @description Packet loss percentage - */ - package_loss: number; - }; - Site: { - /** - * @description Name of the site - * @example Filipino Community Center - */ - name: string; - /** - * Format: double - * @description Geographic latitude coordinate - * @example 47.681932654395915 - */ - latitude: number; - /** - * Format: double - * @description Geographic longitude coordinate - * @example -122.31829217664796 - */ - longitude: number; - /** - * @description Current status of the site - * @example active - * @enum {string} - */ - status: SiteStatus; - /** - * @description Physical address of the site - * @example 5740 Martin Luther King Jr Way S, Seattle, WA 98118 - */ - address: string; - /** @description Array of cell identifiers associated with the site */ - cell_id: string[]; - /** - * @description Optional color identifier for the site in hex code - * @example #FF5733 - */ - color?: string; - /** @description Optional geographical boundary coordinates defining the site perimeter as [latitude, longitude] pairs */ - boundary?: [number, number][]; - }; - /** @example { - * "Filipino Community Center": { - * "ping": 115.28, - * "download_speed": 7.16, - * "upload_speed": 8.63, - * "dbm": -78.4 - * } - * } */ - SitesSummary: { - [key: string]: { + "/api/register": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Registers a new user */ + post: operations["registerUser"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/report_signal": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Report a signal strength measurement. */ + post: operations["reportSignal"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/report_measurement": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Report a speed test measurement. */ + post: operations["reportMeasurement"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/data": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; /** - * Format: double - * @description Average ping in milliseconds - * @example 137.41 + * Retrieve network data + * @description Fetches network data with optional filtering by cell_id or timestamp range, and visual display parameters. When filtering by timestamp, both timestamp_from and timestamp_to must be provided together to define a date range. Results are always sorted by timestamp. */ - ping: number; + get: { + parameters: { + query?: { + /** @description Filter results by cell identifier */ + cell_id?: string; + /** @description Start of timestamp range - must be used together with timestamp_to */ + timestamp_from?: string; + /** @description End of timestamp range - must be used together with timestamp_from */ + timestamp_to?: string; + /** @description Width of the display area in pixels */ + width?: number; + /** @description Height of the display area in pixels */ + height?: number; + /** @description Top coordinate of the viewport */ + top?: number; + /** @description Left coordinate of the viewport */ + left?: number; + /** @description Controls the size of data bins for aggregation */ + binSizeShift?: number; + /** @description Zoom level for the map view */ + zoom?: number; + /** @description Comma-separated list of selected site identifiers */ + selectedSites?: string; + /** @description Type of map visualization to display */ + mapType?: string; + /** @description Alternative format for timestamp_from */ + timeFrom?: string; + /** @description Alternative format for timestamp_to */ + timeTo?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A list of network data records, sorted by timestamp */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["QueryData"][]; + }; + }; + /** @description Invalid input */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sucess": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; /** - * Format: double - * @description Average download speed - * @example 5.23 + * Success response + * @description Returns a success message, typically used as a redirect target after successful authentication */ - download_speed: number; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/failure": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; /** - * Format: double - * @description Average upload speed - * @example 7.28 + * Failure response + * @description Returns an error message, typically used as a redirect target after failed authentication */ - upload_speed: number; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Authorization failure */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sitesSummary": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; /** - * Format: double - * @description Average signal strength in dBm - * @example -85.3 + * Get summary metrics for all sites + * @description Returns average ping, download speed, upload speed, and signal strength for each site within the specified time range */ - dbm: number; - }; - }; - MarkerData: { - /** - * Format: double - * @description Geographic latitude coordinate - * @example 47.681932654395915 - */ - latitude: number; - /** - * Format: double - * @description Geographic longitude coordinate - * @example -122.31829217664796 - */ - longitude: number; - /** - * @description Identifier for the device that collected the data - * @example 1e683a49d71ffd0 - */ - device_id: string; - /** - * @description Name of the site - * @example Filipino Community Center - */ - site: string; - /** - * Format: double - * @description Signal strength in dBm (optional) - * @example -78.4 - */ - dbm?: number; - /** - * Format: double - * @description Upload speed measurement - * @example 7.28 - */ - upload_speed: number; - /** - * Format: double - * @description Download speed measurement - * @example 5.23 - */ - download_speed: number; - /** - * Format: double - * @description Network latency measurement - * @example 137.41 - */ - ping: number; - /** - * @description Measurement identifier - * @example 614157263c28e1a473ede843 - */ - mid: string; - }; - LineSummaryItem: { - /** - * @description Name of the site - * @example Filipino Community Center - */ - site: string; - /** @description Time series data points for the site */ - values: { + get: { + parameters: { + query: { + /** @description Start of the time range */ + timeFrom: string; + /** @description End of the time range */ + timeTo: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Summary metrics for all sites */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SitesSummary"]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/lineSummary": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; /** - * Format: date-time - * @description Timestamp for the data point - * @example 2021-01-25T18:00:00.000Z + * Get time series data for selected sites + * @description Returns time series data for the specified metric and sites within the specified time range */ - date: string; + get: { + parameters: { + query: { + /** @description Type of metric to aggregate (ping, download_speed, upload_speed, or dbm) */ + mapType: string; + /** @description Comma-separated list of site names to include */ + selectedSites: string; + /** @description Start of the time range */ + timeFrom: string; + /** @description End of the time range */ + timeTo: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Time series data for selected sites */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LineSummaryItem"][]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/markers": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; /** - * Format: double - * @description Average value for the metric at this timestamp - * @example 7.28 + * Get marker data for map visualization + * @description Returns geolocation data with network metrics for selected sites and devices within the specified time range */ - value: number; - }[]; - }; - UserRegistration: { - /** - * @description Public key in hexadecimal format, ed25519, pem, pkcs8. - * @example aabbccddeeff00112233445566778899 - */ - publicKey: string; - /** - * Format: byte - * @description Message to be registered in binary format. - * @example SGVsbG8gd29ybGQ= - */ - message: string; - /** - * @description Signature of message in hex. - * @example c3lzdGVtZXN0cmluZw== - */ - sigMessage: string; - }; - DataReport: { - /** @description The parameter `h_pkr` */ - h_pkr: string; - /** @description The parameter `sigma_m` */ - sigma_m: string; - /** @description The parameter `M` */ - M: string; + get: { + parameters: { + query: { + /** @description Comma-separated list of site names to include */ + sites: string; + /** @description Comma-separated list of device IDs to include */ + devices?: string; + /** @description Start of the time range */ + timeFrom: string; + /** @description End of the time range */ + timeTo: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Marker data for map visualization */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MarkerData"][]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/dataRange": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get geographic boundaries of available data + * @description Returns the center coordinates and bounding box (minimum and maximum latitude/longitude) of all available measurement data. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Geographic boundaries of available data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DataRangeResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sites": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get all sites + * @description Returns a list of all available sites with their location and status information + */ + get: operations["getSites"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/secure-site": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Edit an existing site + * @description Updates an existing site with the provided information + */ + put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["Site"]; + }; + }; + responses: { + /** @description Site successfully updated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Site"]; + }; + }; + /** @description Bad request - invalid site data */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Site not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + /** + * Add a new site + * @description Creates a new site with the provided information + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["Site"]; + }; + }; + responses: { + /** @description Site successfully created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Site"]; + }; + }; + /** @description Bad request - invalid site data */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + /** + * Delete a site + * @description Removes an existing site from the system + */ + delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["Site"]; + }; + }; + responses: { + /** @description Site successfully deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Bad request - invalid site data */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Site not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - SiteMeasurementData: { - /** - * @description Unique identifier for the record - * @example 614157263c28e1a473ede843 - */ - _id: string; - /** - * Format: double - * @description Geographic latitude coordinate - * @example 47.681932654395915 - */ - latitude: number; - /** - * Format: double - * @description Geographic longitude coordinate - * @example -122.31829217664796 - */ - longitude: number; - /** - * Format: date-time - * @description Time when the measurement was recorded - * @example 2021-01-25T18:43:54.370Z - */ - timestamp: string; - /** - * Format: double - * @description Upload speed measurement - * @example 7.289173724717997 - */ - upload_speed: number; - /** - * Format: double - * @description Download speed measurement - * @example 5.234371563131994 - */ - download_speed: number; - /** - * Format: double - * @description Amount of data transferred since the previous report - * @example 735.2343217314725 - */ - data_since_last_report: number; - /** - * Format: double - * @description Network latency measurement - * @example 137.41470114174285 - */ - ping: number; - /** - * @description Identifier for the cell/location where data was collected - * @example Filipino Community Center - */ - cell_id: string; - /** - * @description Identifier for the device that collected the data - * @example 1e683a49d71ffd0 - */ - device_id: string; + "/api/public-sites": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get sites list + * @description Returns a list of sites + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of public */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description List of sites */ + sites: components["schemas"]["Site"][]; + }; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/secure/get_groups": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get distinct group identifiers + * @description Returns a list of unique group identifiers across signal and measurement data + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully retrieved group list */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GroupList"]; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/secure/delete_group": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Delete a group of measurements + * @description Removes all measurements associated with the specified group + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DeleteGroupRequest"]; + }; + }; + responses: { + /** @description Successfully deleted group */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/secure/delete_manual": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Delete manual measurements + * @description Removes all manually entered measurements from the database + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully deleted manual measurements */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/secure/upload_data": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Upload and process measurement data + * @description Parses CSV data and stores it as both signal and measurement records. + * If a group is specified, any existing data with that group will be removed first. + * The CSV should include columns for date, time, coordinate, cell_id, dbm, ping, download_speed, and upload_speed. + * + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UploadDataRequest"]; + }; + }; + responses: { + /** @description Successfully uploaded and processed data */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Bad request - missing CSV data or incorrect format */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server error during processing */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Service unavailable - database operation failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/secure/get-users": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get lists of registered and pending users + * @description Returns two lists: + * 1. Registered users sorted by issue date (newest first) + * 2. Pending users whose issue date is within the expiry display limit, sorted by issue date (newest first) + * + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully retrieved user lists */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetUserResponse"]; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/secure/toggle-users": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Toggle a user's enabled status + * @description Enables or disables a user account by their identity + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ToggleUserRequest"]; + }; + }; + responses: { + /** @description Successfully toggled user status */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Bad request - missing or invalid parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Service unavailable - database operation failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/secure/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * LDAP authentication + * @description Authenticates a user against an LDAP directory server. + * Uses Passport LDAP strategy which binds to the LDAP server with the provided credentials. + * On success, creates a session and redirects to /api/success. + * On failure, redirects to /api/failure. + * + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LdapCredentials"]; + "application/x-www-form-urlencoded": components["schemas"]["LdapCredentials"]; + }; + }; + responses: { + /** @description Successful authentication */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example success */ + result: string; + }; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Invalid credentials */ + error: string; + }; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Failed to establish session */ + error: string; + }; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - DataRangeResponse: { - /** - * @description Center coordinates [latitude, longitude] of the data range - * @example [ - * 47.6062, - * -122.3321 - * ] - */ - center: number[]; - /** - * Format: double - * @description Minimum latitude value in the data range - * @example 47.5001 - */ - minLat: number; - /** - * Format: double - * @description Minimum longitude value in the data range - * @example -122.4382 - */ - minLon: number; - /** - * Format: double - * @description Maximum latitude value in the data range - * @example 47.734 - */ - maxLat: number; - /** - * Format: double - * @description Maximum longitude value in the data range - * @example -122.2364 - */ - maxLon: number; + "/api/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Log out user + * @description Ends the user's authenticated session + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully logged out */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/secure/edit_sites": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Update site configuration + * @description Updates the sites configuration file with provided data. + * Requires user to be authenticated - will redirect to login page if not authenticated. + * + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/x-www-form-urlencoded": { + /** + * @description JSON string containing site configuration data + * @example {"site1":{"name":"Site 1","location":"Building A"},"site2":{"name":"Site 2","location":"Building B"}} + */ + sites: string; + }; + }; + }; + responses: { + /** @description Site configuration successfully updated */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Bad request - invalid JSON format or missing required fields */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Unauthorized - User not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Server error while updating configuration file */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/secure/new-user": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create a new user with cryptographic identity + * @description Creates a new user with a cryptographically secure identity using EC keys. + * Generates keypairs, creates signatures, and stores user information. + * Requires authentication - will redirect to login page if not authenticated. + * + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NewUserRequest"]; + }; + }; + responses: { + /** @description User successfully created with cryptographic identity */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NewUserRequest"]; + }; + }; + /** @description Unauthorized - User not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Cryptographic operation error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Database operation error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** @description Represents a single data point in a query result */ - QueryData: { - /** - * @description The bin/bucket identifier for data categorization - * @example 3 - */ - bin: number; - /** - * @description The string representation of calculated average value for data in this bin with 2 decimal places. - * @example 45.70 - */ - average: string; +} +export type webhooks = Record; +export interface components { + schemas: { + NewUserRequest: { + /** + * Format: email + * @description User's email address + * @example user@example.com + */ + email?: string; + /** + * @description User's first name + * @example John + */ + firstName?: string; + /** + * @description User's last name + * @example Doe + */ + lastName?: string; + }; + LdapCredentials: { + /** + * @description LDAP username (could be DN, uid, or email depending on LDAP configuration) + * @example uid=jsmith,ou=users,dc=example,dc=com + */ + username: string; + /** + * Format: password + * @description LDAP password + * @example password123 + */ + password: string; + }; + CryptoIdentityResponse: { + /** + * @description Cryptographic signature in hexadecimal format + * @example 30450221009d41a9afd... + */ + sigma_t?: string; + /** + * @description Private key in DER format converted to hexadecimal + * @example 308184020100301006... + */ + sk_t?: string; + /** + * @description Public key in DER format converted to hexadecimal + * @example 3056301006072a8648... + */ + pk_a?: string; + }; + UploadDataRequest: { + /** + * @description CSV data to be parsed and stored + * @example date,time,coordinate,cell_id,dbm,ping,download_speed,upload_speed + * 2021-01-25,18:43:54,47.681932,-122.318292,cell-1,-85.3,-87.1,137.4,5.2,7.3 + */ + csv: string; + /** + * @description Optional group identifier to associate with uploaded data + * @example fieldtrip-2021 + */ + group?: string; + }; + DeleteGroupRequest: { + /** + * @description Group identifier to delete + * @example fieldtrip-2021 + */ + group: string; + }; + /** @description List of unique group identifiers across signal and measurement data */ + GroupList: string[]; + User: { + /** + * @description Unique identifier for the user + * @example 9a8b7c6d5e4f3g2h1i + */ + identity: string; + /** + * Format: email + * @description User's email address + * @example user@example.com + */ + email: string; + /** + * @description User's first name + * @example John + */ + firstName: string; + /** + * @description User's last name + * @example Doe + */ + lastName: string; + /** + * @description Whether the user has completed registration + * @example true + */ + registered: boolean; + /** + * Format: date-time + * @description Date when the user was issued or account was created + * @example 2023-03-15T14:30:45.123Z + */ + issueDate: string; + /** + * @description Whether the user account is currently enabled + * @example true + */ + isEnabled: boolean; + /** + * @description User's public key + * @example 308201a2300d06092a864886f70d01010105000382018f003082018a02820181... + */ + publicKey: string; + /** + * @description QR code data for user registration + * @example {"sigma_t":"...","sk_t":"...","pk_a":"..."} + */ + qrCode: string; + /** + * Format: date-time + * @description Last time the user was online in ISO 8601 date-time (YYYY-MM-DDTHH:mm:ss.sssZ) + * @example 2023-03-15T14:30:45.123Z + */ + lastOnline: string; + }; + GetUserResponse: { + /** @description List of pending users (not yet registered) whose issue date is within the expiry limit */ + pending: components["schemas"]["User"][]; + /** @description List of registered users */ + registered: components["schemas"]["User"][]; + }; + ToggleUserRequest: { + /** + * @description Unique identifier of the user to update + * @example 9a8b7c6d5e4f3g2h1i + */ + identity: string; + /** + * @description New enabled status for the user + * @example true + */ + enabled: boolean; + }; + BaseMeasureDataModel: { + /** + * Format: double + * @description Geographic latitude coordinate + */ + latitude: number; + /** + * Format: double + * @description Geographic longitude coordinate + */ + longitude: number; + /** @description When the measurement was taken */ + timestamp: string; + /** @description Identifier for the cell tower or access point */ + cell_id: string; + /** @description Unique identifier for the reporting device */ + device_id: string; + /** @description Flag indicating if the measurement has been reported and will be shown on the map. */ + show_data: boolean; + }; + SignalStrengthReportModel: components["schemas"]["BaseMeasureDataModel"] & { + /** @description Signal strength in decibel-milliwatts */ + dbm: number; + /** @description Code representing the signal strength level */ + level_code: number; + }; + ConnectivityReportModel: components["schemas"]["BaseMeasureDataModel"] & { + /** + * Format: double + * @description Upload speed measurement + */ + upload_speed: number; + /** + * Format: double + * @description Download speed measurement + */ + download_speed: number; + /** + * Format: double + * @description Network latency in milliseconds + */ + ping: number; + /** + * Format: double + * @description Packet loss percentage + */ + package_loss: number; + }; + Site: { + /** + * @description Name of the site + * @example Filipino Community Center + */ + name: string; + /** + * Format: double + * @description Geographic latitude coordinate + * @example 47.681932654395915 + */ + latitude: number; + /** + * Format: double + * @description Geographic longitude coordinate + * @example -122.31829217664796 + */ + longitude: number; + /** + * @description Current status of the site + * @example active + * @enum {string} + */ + status: SiteStatus; + /** + * @description Physical address of the site + * @example 5740 Martin Luther King Jr Way S, Seattle, WA 98118 + */ + address: string; + /** @description Array of cell identifiers associated with the site */ + cell_id: string[]; + /** + * @description Optional color identifier for the site in hex code + * @example #FF5733 + */ + color?: string; + /** @description Optional geographical boundary coordinates defining the site perimeter as [latitude, longitude] pairs */ + boundary?: [ + number, + number + ][]; + }; + /** @example { + * "Filipino Community Center": { + * "ping": 115.28, + * "download_speed": 7.16, + * "upload_speed": 8.63, + * "dbm": -78.4 + * } + * } */ + SitesSummary: { + [key: string]: { + /** + * Format: double + * @description Average ping in milliseconds + * @example 137.41 + */ + ping: number; + /** + * Format: double + * @description Average download speed + * @example 5.23 + */ + download_speed: number; + /** + * Format: double + * @description Average upload speed + * @example 7.28 + */ + upload_speed: number; + /** + * Format: double + * @description Average signal strength in dBm + * @example -85.3 + */ + dbm: number; + }; + }; + MarkerData: { + /** + * Format: double + * @description Geographic latitude coordinate + * @example 47.681932654395915 + */ + latitude: number; + /** + * Format: double + * @description Geographic longitude coordinate + * @example -122.31829217664796 + */ + longitude: number; + /** + * @description Identifier for the device that collected the data + * @example 1e683a49d71ffd0 + */ + device_id: string; + /** + * @description Name of the site + * @example Filipino Community Center + */ + site: string; + /** + * Format: double + * @description Signal strength in dBm (optional) + * @example -78.4 + */ + dbm?: number; + /** + * Format: double + * @description Upload speed measurement + * @example 7.28 + */ + upload_speed: number; + /** + * Format: double + * @description Download speed measurement + * @example 5.23 + */ + download_speed: number; + /** + * Format: double + * @description Network latency measurement + * @example 137.41 + */ + ping: number; + /** + * @description Measurement identifier + * @example 614157263c28e1a473ede843 + */ + mid: string; + }; + LineSummaryItem: { + /** + * @description Name of the site + * @example Filipino Community Center + */ + site: string; + /** @description Time series data points for the site */ + values: { + /** + * Format: date-time + * @description Timestamp for the data point + * @example 2021-01-25T18:00:00.000Z + */ + date: string; + /** + * Format: double + * @description Average value for the metric at this timestamp + * @example 7.28 + */ + value: number; + }[]; + }; + UserRegistration: { + /** + * @description Public key in hexadecimal format, ed25519, pem, pkcs8. + * @example aabbccddeeff00112233445566778899 + */ + publicKey: string; + /** + * Format: byte + * @description Message to be registered in binary format. + * @example SGVsbG8gd29ybGQ= + */ + message: string; + /** + * @description Signature of message in hex. + * @example c3lzdGVtZXN0cmluZw== + */ + sigMessage: string; + }; + DataReport: { + /** @description The parameter `h_pkr` */ + h_pkr: string; + /** @description The parameter `sigma_m` */ + sigma_m: string; + /** @description The parameter `M` */ + M: string; + }; + SiteMeasurementData: { + /** + * @description Unique identifier for the record + * @example 614157263c28e1a473ede843 + */ + _id: string; + /** + * Format: double + * @description Geographic latitude coordinate + * @example 47.681932654395915 + */ + latitude: number; + /** + * Format: double + * @description Geographic longitude coordinate + * @example -122.31829217664796 + */ + longitude: number; + /** + * Format: date-time + * @description Time when the measurement was recorded + * @example 2021-01-25T18:43:54.370Z + */ + timestamp: string; + /** + * Format: double + * @description Upload speed measurement + * @example 7.289173724717997 + */ + upload_speed: number; + /** + * Format: double + * @description Download speed measurement + * @example 5.234371563131994 + */ + download_speed: number; + /** + * Format: double + * @description Amount of data transferred since the previous report + * @example 735.2343217314725 + */ + data_since_last_report: number; + /** + * Format: double + * @description Network latency measurement + * @example 137.41470114174285 + */ + ping: number; + /** + * @description Identifier for the cell/location where data was collected + * @example Filipino Community Center + */ + cell_id: string; + /** + * @description Identifier for the device that collected the data + * @example 1e683a49d71ffd0 + */ + device_id: string; + }; + DataRangeResponse: { + /** + * @description Center coordinates [latitude, longitude] of the data range + * @example [ + * 47.6062, + * -122.3321 + * ] + */ + center: number[]; + /** + * Format: double + * @description Minimum latitude value in the data range + * @example 47.5001 + */ + minLat: number; + /** + * Format: double + * @description Minimum longitude value in the data range + * @example -122.4382 + */ + minLon: number; + /** + * Format: double + * @description Maximum latitude value in the data range + * @example 47.734 + */ + maxLat: number; + /** + * Format: double + * @description Maximum longitude value in the data range + * @example -122.2364 + */ + maxLon: number; + }; + /** @description Represents a single data point in a query result */ + QueryData: { + /** + * @description The bin/bucket identifier for data categorization + * @example 3 + */ + bin: number; + /** + * @description The string representation of calculated average value for data in this bin with 2 decimal places. + * @example 45.70 + */ + average: string; + }; }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; export interface operations { - registerUser: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['UserRegistration']; - }; - }; - responses: { - /** @description User already registered */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description User registered successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid input */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized registration */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Registration keys are invalid */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Registration period expired */ - 408: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Internal server error */ - 503: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - reportSignal: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['DataReport']; - }; - }; - responses: { - /** @description Signal reported successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description User not found */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid signature */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - reportMeasurement: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['DataReport']; - }; - }; - responses: { - /** @description Signal reported successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description User not found */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid signature */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; + registerUser: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UserRegistration"]; + }; + }; + responses: { + /** @description User already registered */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description User registered successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid input */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized registration */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Registration keys are invalid */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Registration period expired */ + 408: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; }; - }; - getSites: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + reportSignal: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DataReport"]; + }; + }; + responses: { + /** @description Signal reported successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description User not found */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid signature */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; }; - requestBody?: never; - responses: { - /** @description List of sites */ - 200: { - headers: { - [name: string]: unknown; + reportMeasurement: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DataReport"]; + }; }; - content: { - 'application/json': components['schemas']['Site'][]; + responses: { + /** @description Signal reported successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description User not found */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid signature */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getSites: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of sites */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Site"][]; + }; + }; }; - }; }; - }; } export enum SiteStatus { - active = 'active', - confirmed = 'confirmed', - in_conversation = 'in-conversation', + active = "active", + confirmed = "confirmed", + in_conversation = "in-conversation" } From 6d7ebd97ba3e5c7a13af831869e25a8b139c2d1e Mon Sep 17 00:00:00 2001 From: Ananya Date: Sat, 12 Jul 2025 00:51:40 -0700 Subject: [PATCH 03/24] feat(core): add mongoose schema for Site model add mongoose schema to represent Site. includes support for required fields such as name, coordinates, status, address, and cell_id, as well as optional fields like color and boundary with validation. --- src/models/site.ts | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/models/site.ts diff --git a/src/models/site.ts b/src/models/site.ts new file mode 100644 index 0000000..94414fd --- /dev/null +++ b/src/models/site.ts @@ -0,0 +1,65 @@ +import mongoose from 'mongoose'; +import { components } from '../types/schema'; + +type ISite = components['schemas']['Site']; + +interface SiteDoc extends mongoose.Document, ISite {} + +interface SiteModelInterface extends mongoose.Model { + build(attr: ISite): SiteDoc; +} + +const siteSchema = new mongoose.Schema( + { + name: { + type: String, + required: true, + }, + latitude: { + type: Number, + required: true, + }, + longitude: { + type: Number, + required: true, + }, + status: { + type: String, + enum: ['active', 'confirmed', 'in-conversation'], + required: true, + }, + address: { + type: String, + required: true, + }, + cell_id: { + type: [String], + required: true, + }, + color: { + type: String, + required: false, + }, + boundary: { + type: [[Number]], + validate: { + validator: function (val: number[][]) { + return val.every(pair => pair.length === 2); + }, + message: 'Each boundary coordinate must be a pair of [latitude, longitude]', + }, + required: false, + }, + }, + { + versionKey: false, + } +); + +siteSchema.statics.build = (attr: ISite) => { + return new Site(attr); +}; + +const Site = mongoose.model('Site', siteSchema); + +export { Site, ISite, SiteDoc }; From 8230e28f9c1df79bbd72a7bf39443c859caba3a7 Mon Sep 17 00:00:00 2001 From: Ananya Date: Tue, 15 Jul 2025 21:30:17 -0700 Subject: [PATCH 04/24] feat: add secure site management endpoints - Add POST /api/secure-site to create new sites - Add PUT /api/secure-site to update existing sites - Add DELETE /api/secure-site to remove sites --- src/index.ts | 2 + src/routes/secure-site.ts | 90 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/routes/secure-site.ts diff --git a/src/index.ts b/src/index.ts index 4848d3d..2f49c07 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import { ldapRouter } from './routes/ldap-login'; import { newUserRouter } from './routes/new-user'; import { usersRouter } from './routes/users'; import { editSitesRouter } from './routes/edit-sites'; +import { secureDataRouter } from './routes/secure-site'; import logger from './logger'; import cors from 'cors'; @@ -72,6 +73,7 @@ app.use(ldapRouter); app.use(newUserRouter); app.use(usersRouter); app.use(editSitesRouter); +app.use(secureDataRouter); process.on('SIGINT', async () => { await mongoose.connection.close(); diff --git a/src/routes/secure-site.ts b/src/routes/secure-site.ts new file mode 100644 index 0000000..528661c --- /dev/null +++ b/src/routes/secure-site.ts @@ -0,0 +1,90 @@ +import express, { Request, Response } from 'express'; +import { Site } from '../models/site'; +// import connectEnsureLogin from 'connect-ensure-login'; +import { components } from '../types/schema'; + +const router = express.Router(); + +type Site = components['schemas']['Site']; + + +router.put( + 'api/secure-site', +// connectEnsureLogin.ensureLoggedIn('/api/failure'), + async (req: Request, res: Response) => { + try { + const siteData: Site = req.body; + + if (!siteData.name) { + res.status(400).json({ error: 'Site name is required' }); + return; + } + + const updatedSite = await Site.findOneAndUpdate( + { name: siteData.name }, + siteData, + { new: true, runValidators: true } + ); + + if (!updatedSite) { + res.status(404).json({ error: 'Site not found' }); + return; + } + + res.status(200).json(updatedSite); + } catch (error: any) { + res.status(500).json({ error: 'Internal server error' }); + } + } +); + + +router.post( + 'api/secure-site', +// connectEnsureLogin.ensureLoggedIn('/api/failure'), + async (req: Request, res: Response) => { + try { + const siteData: Site = req.body; + + const newSite = Site.build(siteData); + const savedSite = await newSite.save(); + + res.status(201).json(savedSite); + } catch (error: any) { + if (error.name === 'ValidationError') { + res.status(400).json({ error: 'Validation error', details: error.message }); + return; + } + res.status(500).json({ error: 'Internal server error' }); + } + } +); + + +router.delete( + 'api/secure-site', +// connectEnsureLogin.ensureLoggedIn('/api/failure'), + async (req: Request, res: Response) => { + try { + const siteData: Site = req.body; + + if (!siteData.name) { + res.status(400).json({ error: 'Site name is required' }); + return; + } + + const deletedSite = await Site.findOneAndDelete({ name: siteData.name }); + + if (!deletedSite) { + res.status(404).json({ error: 'Site not found' }); + return; + } + + res.status(200).json({ message: 'Site deleted successfully', site: deletedSite }); + } catch (error: any) { + res.status(500).json({ error: 'Internal server error' }); + } + } +); + +export { router as secureDataRouter }; From 0992445c233dbdd181500565a35070a3ad83bcea Mon Sep 17 00:00:00 2001 From: Ananya Date: Wed, 16 Jul 2025 03:30:51 -0700 Subject: [PATCH 05/24] fix(routes): add leading slash to secure-site endpoints Routes were missing leading slash causing 404 errors for /api/secure-site --- src/routes/secure-site.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/secure-site.ts b/src/routes/secure-site.ts index 528661c..c73d1a6 100644 --- a/src/routes/secure-site.ts +++ b/src/routes/secure-site.ts @@ -9,7 +9,7 @@ type Site = components['schemas']['Site']; router.put( - 'api/secure-site', + '/api/secure-site', // connectEnsureLogin.ensureLoggedIn('/api/failure'), async (req: Request, res: Response) => { try { @@ -40,7 +40,7 @@ router.put( router.post( - 'api/secure-site', + '/api/secure-site', // connectEnsureLogin.ensureLoggedIn('/api/failure'), async (req: Request, res: Response) => { try { @@ -62,7 +62,7 @@ router.post( router.delete( - 'api/secure-site', + '/api/secure-site', // connectEnsureLogin.ensureLoggedIn('/api/failure'), async (req: Request, res: Response) => { try { From 1c217d47b3a075d4bcb1ff059c074e476942a76c Mon Sep 17 00:00:00 2001 From: Ananya Date: Wed, 16 Jul 2025 08:03:20 -0700 Subject: [PATCH 06/24] feat: add public sites API endpoint Create /api/public-sites GET route that returns list of sites --- src/index.ts | 6 ++++-- src/routes/public-sites.ts | 19 +++++++++++++++++++ src/routes/secure-site.ts | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 src/routes/public-sites.ts diff --git a/src/index.ts b/src/index.ts index 2f49c07..77f09b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,8 @@ import { ldapRouter } from './routes/ldap-login'; import { newUserRouter } from './routes/new-user'; import { usersRouter } from './routes/users'; import { editSitesRouter } from './routes/edit-sites'; -import { secureDataRouter } from './routes/secure-site'; +import { secureSitesRouter } from './routes/secure-site'; +import { publicSitesRouter } from './routes/public-sites'; import logger from './logger'; import cors from 'cors'; @@ -73,7 +74,8 @@ app.use(ldapRouter); app.use(newUserRouter); app.use(usersRouter); app.use(editSitesRouter); -app.use(secureDataRouter); +app.use(secureSitesRouter); +app.use(publicSitesRouter); process.on('SIGINT', async () => { await mongoose.connection.close(); diff --git a/src/routes/public-sites.ts b/src/routes/public-sites.ts new file mode 100644 index 0000000..9f54f58 --- /dev/null +++ b/src/routes/public-sites.ts @@ -0,0 +1,19 @@ +import express, { Request, Response } from 'express'; +import { Site } from '../models/site'; + +const router = express.Router(); + +router.get('/api/public-sites', async (req: Request, res: Response) => { + try { + const sites = await Site.find(); + + res.status(200).json({ + sites: sites + }); + } catch (error) { + console.error('Error retrieving public sites:', error); + res.status(500).send('Internal server error'); + } +}); + +export { router as publicSitesRouter }; diff --git a/src/routes/secure-site.ts b/src/routes/secure-site.ts index c73d1a6..1019493 100644 --- a/src/routes/secure-site.ts +++ b/src/routes/secure-site.ts @@ -87,4 +87,4 @@ router.delete( } ); -export { router as secureDataRouter }; +export { router as secureSitesRouter }; From 6e35be77b0c7bd9c3d0445a080d52a758e0963c6 Mon Sep 17 00:00:00 2001 From: Ananya Date: Wed, 16 Jul 2025 22:14:30 -0700 Subject: [PATCH 07/24] refactor: address PR review comments Apply suggested changes from code review --- src/models/site.ts | 5 +- src/routes/public-sites.ts | 4 +- src/routes/secure-site.ts | 41 +- src/types/schema.d.ts | 3897 ++++++++++++++++++------------------ 4 files changed, 1973 insertions(+), 1974 deletions(-) diff --git a/src/models/site.ts b/src/models/site.ts index 94414fd..1a108d8 100644 --- a/src/models/site.ts +++ b/src/models/site.ts @@ -46,14 +46,15 @@ const siteSchema = new mongoose.Schema( validator: function (val: number[][]) { return val.every(pair => pair.length === 2); }, - message: 'Each boundary coordinate must be a pair of [latitude, longitude]', + message: + 'Each boundary coordinate must be a pair of [latitude, longitude]', }, required: false, }, }, { versionKey: false, - } + }, ); siteSchema.statics.build = (attr: ISite) => { diff --git a/src/routes/public-sites.ts b/src/routes/public-sites.ts index 9f54f58..3bb04fd 100644 --- a/src/routes/public-sites.ts +++ b/src/routes/public-sites.ts @@ -6,9 +6,9 @@ const router = express.Router(); router.get('/api/public-sites', async (req: Request, res: Response) => { try { const sites = await Site.find(); - + res.status(200).json({ - sites: sites + sites: sites, }); } catch (error) { console.error('Error retrieving public sites:', error); diff --git a/src/routes/secure-site.ts b/src/routes/secure-site.ts index 1019493..572c5ff 100644 --- a/src/routes/secure-site.ts +++ b/src/routes/secure-site.ts @@ -5,16 +5,15 @@ import { components } from '../types/schema'; const router = express.Router(); -type Site = components['schemas']['Site']; - +type SiteRequest = components['schemas']['Site']; router.put( '/api/secure-site', -// connectEnsureLogin.ensureLoggedIn('/api/failure'), + // connectEnsureLogin.ensureLoggedIn('/api/failure'), async (req: Request, res: Response) => { try { - const siteData: Site = req.body; - + const siteData: SiteRequest = req.body; + if (!siteData.name) { res.status(400).json({ error: 'Site name is required' }); return; @@ -23,7 +22,7 @@ router.put( const updatedSite = await Site.findOneAndUpdate( { name: siteData.name }, siteData, - { new: true, runValidators: true } + { new: true, runValidators: true }, ); if (!updatedSite) { @@ -31,43 +30,43 @@ router.put( return; } - res.status(200).json(updatedSite); + res.status(200).json({ message: 'Site updated successfully' }); } catch (error: any) { res.status(500).json({ error: 'Internal server error' }); } - } + }, ); - router.post( '/api/secure-site', -// connectEnsureLogin.ensureLoggedIn('/api/failure'), + // connectEnsureLogin.ensureLoggedIn('/api/failure'), async (req: Request, res: Response) => { try { - const siteData: Site = req.body; - + const siteData: SiteRequest = req.body; + const newSite = Site.build(siteData); const savedSite = await newSite.save(); res.status(201).json(savedSite); } catch (error: any) { if (error.name === 'ValidationError') { - res.status(400).json({ error: 'Validation error', details: error.message }); + res + .status(400) + .json({ error: 'Validation error', details: error.message }); return; } res.status(500).json({ error: 'Internal server error' }); } - } + }, ); - router.delete( '/api/secure-site', -// connectEnsureLogin.ensureLoggedIn('/api/failure'), + // connectEnsureLogin.ensureLoggedIn('/api/failure'), async (req: Request, res: Response) => { try { - const siteData: Site = req.body; - + const siteData: SiteRequest = req.body; + if (!siteData.name) { res.status(400).json({ error: 'Site name is required' }); return; @@ -80,11 +79,13 @@ router.delete( return; } - res.status(200).json({ message: 'Site deleted successfully', site: deletedSite }); + res + .status(200) + .json({ message: 'Site deleted successfully', site: deletedSite }); } catch (error: any) { res.status(500).json({ error: 'Internal server error' }); } - } + }, ); export { router as secureSitesRouter }; diff --git a/src/types/schema.d.ts b/src/types/schema.d.ts index d4267db..f04c367 100644 --- a/src/types/schema.d.ts +++ b/src/types/schema.d.ts @@ -4,1980 +4,1977 @@ */ export interface paths { - "/api/register": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Registers a new user */ - post: operations["registerUser"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/report_signal": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Report a signal strength measurement. */ - post: operations["reportSignal"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/report_measurement": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Report a speed test measurement. */ - post: operations["reportMeasurement"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/data": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Retrieve network data - * @description Fetches network data with optional filtering by cell_id or timestamp range, and visual display parameters. When filtering by timestamp, both timestamp_from and timestamp_to must be provided together to define a date range. Results are always sorted by timestamp. - */ - get: { - parameters: { - query?: { - /** @description Filter results by cell identifier */ - cell_id?: string; - /** @description Start of timestamp range - must be used together with timestamp_to */ - timestamp_from?: string; - /** @description End of timestamp range - must be used together with timestamp_from */ - timestamp_to?: string; - /** @description Width of the display area in pixels */ - width?: number; - /** @description Height of the display area in pixels */ - height?: number; - /** @description Top coordinate of the viewport */ - top?: number; - /** @description Left coordinate of the viewport */ - left?: number; - /** @description Controls the size of data bins for aggregation */ - binSizeShift?: number; - /** @description Zoom level for the map view */ - zoom?: number; - /** @description Comma-separated list of selected site identifiers */ - selectedSites?: string; - /** @description Type of map visualization to display */ - mapType?: string; - /** @description Alternative format for timestamp_from */ - timeFrom?: string; - /** @description Alternative format for timestamp_to */ - timeTo?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description A list of network data records, sorted by timestamp */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["QueryData"][]; - }; - }; - /** @description Invalid input */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/sucess": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Success response - * @description Returns a success message, typically used as a redirect target after successful authentication - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/failure": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Failure response - * @description Returns an error message, typically used as a redirect target after failed authentication - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Authorization failure */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/sitesSummary": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get summary metrics for all sites - * @description Returns average ping, download speed, upload speed, and signal strength for each site within the specified time range - */ - get: { - parameters: { - query: { - /** @description Start of the time range */ - timeFrom: string; - /** @description End of the time range */ - timeTo: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Summary metrics for all sites */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SitesSummary"]; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/lineSummary": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get time series data for selected sites - * @description Returns time series data for the specified metric and sites within the specified time range - */ - get: { - parameters: { - query: { - /** @description Type of metric to aggregate (ping, download_speed, upload_speed, or dbm) */ - mapType: string; - /** @description Comma-separated list of site names to include */ - selectedSites: string; - /** @description Start of the time range */ - timeFrom: string; - /** @description End of the time range */ - timeTo: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Time series data for selected sites */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["LineSummaryItem"][]; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/markers": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get marker data for map visualization - * @description Returns geolocation data with network metrics for selected sites and devices within the specified time range - */ - get: { - parameters: { - query: { - /** @description Comma-separated list of site names to include */ - sites: string; - /** @description Comma-separated list of device IDs to include */ - devices?: string; - /** @description Start of the time range */ - timeFrom: string; - /** @description End of the time range */ - timeTo: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Marker data for map visualization */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["MarkerData"][]; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/dataRange": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get geographic boundaries of available data - * @description Returns the center coordinates and bounding box (minimum and maximum latitude/longitude) of all available measurement data. - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Geographic boundaries of available data */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["DataRangeResponse"]; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/sites": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get all sites - * @description Returns a list of all available sites with their location and status information - */ - get: operations["getSites"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/secure-site": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** - * Edit an existing site - * @description Updates an existing site with the provided information - */ - put: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["Site"]; - }; - }; - responses: { - /** @description Site successfully updated */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Site"]; - }; - }; - /** @description Bad request - invalid site data */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Site not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - /** - * Add a new site - * @description Creates a new site with the provided information - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["Site"]; - }; - }; - responses: { - /** @description Site successfully created */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Site"]; - }; - }; - /** @description Bad request - invalid site data */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - /** - * Delete a site - * @description Removes an existing site from the system - */ - delete: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["Site"]; - }; - }; - responses: { - /** @description Site successfully deleted */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Bad request - invalid site data */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Site not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - options?: never; - head?: never; - patch?: never; - trace?: never; + '/api/register': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - "/api/public-sites": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get sites list - * @description Returns a list of sites - */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of public */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @description List of sites */ - sites: components["schemas"]["Site"][]; - }; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/secure/get_groups": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Get distinct group identifiers - * @description Returns a list of unique group identifiers across signal and measurement data - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully retrieved group list */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GroupList"]; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/secure/delete_group": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Delete a group of measurements - * @description Removes all measurements associated with the specified group - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["DeleteGroupRequest"]; - }; - }; - responses: { - /** @description Successfully deleted group */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/secure/delete_manual": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Delete manual measurements - * @description Removes all manually entered measurements from the database - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully deleted manual measurements */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/secure/upload_data": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; + get?: never; + put?: never; + /** Registers a new user */ + post: operations['registerUser']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/report_signal': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Report a signal strength measurement. */ + post: operations['reportSignal']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/report_measurement': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Report a speed test measurement. */ + post: operations['reportMeasurement']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/data': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Retrieve network data + * @description Fetches network data with optional filtering by cell_id or timestamp range, and visual display parameters. When filtering by timestamp, both timestamp_from and timestamp_to must be provided together to define a date range. Results are always sorted by timestamp. + */ + get: { + parameters: { + query?: { + /** @description Filter results by cell identifier */ + cell_id?: string; + /** @description Start of timestamp range - must be used together with timestamp_to */ + timestamp_from?: string; + /** @description End of timestamp range - must be used together with timestamp_from */ + timestamp_to?: string; + /** @description Width of the display area in pixels */ + width?: number; + /** @description Height of the display area in pixels */ + height?: number; + /** @description Top coordinate of the viewport */ + top?: number; + /** @description Left coordinate of the viewport */ + left?: number; + /** @description Controls the size of data bins for aggregation */ + binSizeShift?: number; + /** @description Zoom level for the map view */ + zoom?: number; + /** @description Comma-separated list of selected site identifiers */ + selectedSites?: string; + /** @description Type of map visualization to display */ + mapType?: string; + /** @description Alternative format for timestamp_from */ + timeFrom?: string; + /** @description Alternative format for timestamp_to */ + timeTo?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A list of network data records, sorted by timestamp */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['QueryData'][]; + }; + }; + /** @description Invalid input */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/sucess': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Success response + * @description Returns a success message, typically used as a redirect target after successful authentication + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/failure': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Failure response + * @description Returns an error message, typically used as a redirect target after failed authentication + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Authorization failure */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/sitesSummary': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get summary metrics for all sites + * @description Returns average ping, download speed, upload speed, and signal strength for each site within the specified time range + */ + get: { + parameters: { + query: { + /** @description Start of the time range */ + timeFrom: string; + /** @description End of the time range */ + timeTo: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Summary metrics for all sites */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SitesSummary']; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/lineSummary': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get time series data for selected sites + * @description Returns time series data for the specified metric and sites within the specified time range + */ + get: { + parameters: { + query: { + /** @description Type of metric to aggregate (ping, download_speed, upload_speed, or dbm) */ + mapType: string; + /** @description Comma-separated list of site names to include */ + selectedSites: string; + /** @description Start of the time range */ + timeFrom: string; + /** @description End of the time range */ + timeTo: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Time series data for selected sites */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['LineSummaryItem'][]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/markers': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get marker data for map visualization + * @description Returns geolocation data with network metrics for selected sites and devices within the specified time range + */ + get: { + parameters: { + query: { + /** @description Comma-separated list of site names to include */ + sites: string; + /** @description Comma-separated list of device IDs to include */ + devices?: string; + /** @description Start of the time range */ + timeFrom: string; + /** @description End of the time range */ + timeTo: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Marker data for map visualization */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['MarkerData'][]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/dataRange': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get geographic boundaries of available data + * @description Returns the center coordinates and bounding box (minimum and maximum latitude/longitude) of all available measurement data. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Geographic boundaries of available data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['DataRangeResponse']; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/sites': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get all sites + * @description Returns a list of all available sites with their location and status information + */ + get: operations['getSites']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/secure-site': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Edit an existing site + * @description Updates an existing site with the provided information + */ + put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Site']; + }; + }; + responses: { + /** @description Site successfully updated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Site']; + }; + }; + /** @description Bad request - invalid site data */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Site not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + /** + * Add a new site + * @description Creates a new site with the provided information + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Site']; + }; + }; + responses: { + /** @description Site successfully created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Site']; + }; + }; + /** @description Bad request - invalid site data */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + /** + * Delete a site + * @description Removes an existing site from the system + */ + delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Site']; + }; + }; + responses: { + /** @description Site successfully deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Bad request - invalid site data */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Site not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/public-sites': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get sites list + * @description Returns a list of sites + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of public */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @description List of sites */ + sites: components['schemas']['Site'][]; + }; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/secure/get_groups': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get distinct group identifiers + * @description Returns a list of unique group identifiers across signal and measurement data + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully retrieved group list */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GroupList']; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/secure/delete_group': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Delete a group of measurements + * @description Removes all measurements associated with the specified group + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['DeleteGroupRequest']; + }; + }; + responses: { + /** @description Successfully deleted group */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/secure/delete_manual': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Delete manual measurements + * @description Removes all manually entered measurements from the database + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully deleted manual measurements */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/secure/upload_data': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Upload and process measurement data + * @description Parses CSV data and stores it as both signal and measurement records. + * If a group is specified, any existing data with that group will be removed first. + * The CSV should include columns for date, time, coordinate, cell_id, dbm, ping, download_speed, and upload_speed. + * + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['UploadDataRequest']; + }; + }; + responses: { + /** @description Successfully uploaded and processed data */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Bad request - missing CSV data or incorrect format */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server error during processing */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Service unavailable - database operation failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/secure/get-users': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get lists of registered and pending users + * @description Returns two lists: + * 1. Registered users sorted by issue date (newest first) + * 2. Pending users whose issue date is within the expiry display limit, sorted by issue date (newest first) + * + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully retrieved user lists */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetUserResponse']; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/secure/toggle-users': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Toggle a user's enabled status + * @description Enables or disables a user account by their identity + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['ToggleUserRequest']; + }; + }; + responses: { + /** @description Successfully toggled user status */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Bad request - missing or invalid parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Unauthorized - redirects to /api/failure */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Service unavailable - database operation failed */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/secure/login': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * LDAP authentication + * @description Authenticates a user against an LDAP directory server. + * Uses Passport LDAP strategy which binds to the LDAP server with the provided credentials. + * On success, creates a session and redirects to /api/success. + * On failure, redirects to /api/failure. + * + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['LdapCredentials']; + 'application/x-www-form-urlencoded': components['schemas']['LdapCredentials']; + }; + }; + responses: { + /** @description Successful authentication */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example success */ + result: string; + }; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example Invalid credentials */ + error: string; + }; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @example Failed to establish session */ + error: string; + }; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/logout': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Log out user + * @description Ends the user's authenticated session + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully logged out */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/secure/edit_sites': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Update site configuration + * @description Updates the sites configuration file with provided data. + * Requires user to be authenticated - will redirect to login page if not authenticated. + * + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/x-www-form-urlencoded': { + /** + * @description JSON string containing site configuration data + * @example {"site1":{"name":"Site 1","location":"Building A"},"site2":{"name":"Site 2","location":"Building B"}} + */ + sites: string; + }; + }; + }; + responses: { + /** @description Site configuration successfully updated */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Bad request - invalid JSON format or missing required fields */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Unauthorized - User not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Server error while updating configuration file */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/secure/new-user': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create a new user with cryptographic identity + * @description Creates a new user with a cryptographically secure identity using EC keys. + * Generates keypairs, creates signatures, and stores user information. + * Requires authentication - will redirect to login page if not authenticated. + * + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['NewUserRequest']; + }; + }; + responses: { + /** @description User successfully created with cryptographic identity */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['NewUserRequest']; + }; + }; + /** @description Unauthorized - User not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Cryptographic operation error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Database operation error */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + NewUserRequest: { + /** + * Format: email + * @description User's email address + * @example user@example.com + */ + email?: string; + /** + * @description User's first name + * @example John + */ + firstName?: string; + /** + * @description User's last name + * @example Doe + */ + lastName?: string; + }; + LdapCredentials: { + /** + * @description LDAP username (could be DN, uid, or email depending on LDAP configuration) + * @example uid=jsmith,ou=users,dc=example,dc=com + */ + username: string; + /** + * Format: password + * @description LDAP password + * @example password123 + */ + password: string; + }; + CryptoIdentityResponse: { + /** + * @description Cryptographic signature in hexadecimal format + * @example 30450221009d41a9afd... + */ + sigma_t?: string; + /** + * @description Private key in DER format converted to hexadecimal + * @example 308184020100301006... + */ + sk_t?: string; + /** + * @description Public key in DER format converted to hexadecimal + * @example 3056301006072a8648... + */ + pk_a?: string; + }; + UploadDataRequest: { + /** + * @description CSV data to be parsed and stored + * @example date,time,coordinate,cell_id,dbm,ping,download_speed,upload_speed + * 2021-01-25,18:43:54,47.681932,-122.318292,cell-1,-85.3,-87.1,137.4,5.2,7.3 + */ + csv: string; + /** + * @description Optional group identifier to associate with uploaded data + * @example fieldtrip-2021 + */ + group?: string; + }; + DeleteGroupRequest: { + /** + * @description Group identifier to delete + * @example fieldtrip-2021 + */ + group: string; + }; + /** @description List of unique group identifiers across signal and measurement data */ + GroupList: string[]; + User: { + /** + * @description Unique identifier for the user + * @example 9a8b7c6d5e4f3g2h1i + */ + identity: string; + /** + * Format: email + * @description User's email address + * @example user@example.com + */ + email: string; + /** + * @description User's first name + * @example John + */ + firstName: string; + /** + * @description User's last name + * @example Doe + */ + lastName: string; + /** + * @description Whether the user has completed registration + * @example true + */ + registered: boolean; + /** + * Format: date-time + * @description Date when the user was issued or account was created + * @example 2023-03-15T14:30:45.123Z + */ + issueDate: string; + /** + * @description Whether the user account is currently enabled + * @example true + */ + isEnabled: boolean; + /** + * @description User's public key + * @example 308201a2300d06092a864886f70d01010105000382018f003082018a02820181... + */ + publicKey: string; + /** + * @description QR code data for user registration + * @example {"sigma_t":"...","sk_t":"...","pk_a":"..."} + */ + qrCode: string; + /** + * Format: date-time + * @description Last time the user was online in ISO 8601 date-time (YYYY-MM-DDTHH:mm:ss.sssZ) + * @example 2023-03-15T14:30:45.123Z + */ + lastOnline: string; + }; + GetUserResponse: { + /** @description List of pending users (not yet registered) whose issue date is within the expiry limit */ + pending: components['schemas']['User'][]; + /** @description List of registered users */ + registered: components['schemas']['User'][]; + }; + ToggleUserRequest: { + /** + * @description Unique identifier of the user to update + * @example 9a8b7c6d5e4f3g2h1i + */ + identity: string; + /** + * @description New enabled status for the user + * @example true + */ + enabled: boolean; + }; + BaseMeasureDataModel: { + /** + * Format: double + * @description Geographic latitude coordinate + */ + latitude: number; + /** + * Format: double + * @description Geographic longitude coordinate + */ + longitude: number; + /** @description When the measurement was taken */ + timestamp: string; + /** @description Identifier for the cell tower or access point */ + cell_id: string; + /** @description Unique identifier for the reporting device */ + device_id: string; + /** @description Flag indicating if the measurement has been reported and will be shown on the map. */ + show_data: boolean; + }; + SignalStrengthReportModel: components['schemas']['BaseMeasureDataModel'] & { + /** @description Signal strength in decibel-milliwatts */ + dbm: number; + /** @description Code representing the signal strength level */ + level_code: number; + }; + ConnectivityReportModel: components['schemas']['BaseMeasureDataModel'] & { + /** + * Format: double + * @description Upload speed measurement + */ + upload_speed: number; + /** + * Format: double + * @description Download speed measurement + */ + download_speed: number; + /** + * Format: double + * @description Network latency in milliseconds + */ + ping: number; + /** + * Format: double + * @description Packet loss percentage + */ + package_loss: number; + }; + Site: { + /** + * @description Name of the site + * @example Filipino Community Center + */ + name: string; + /** + * Format: double + * @description Geographic latitude coordinate + * @example 47.681932654395915 + */ + latitude: number; + /** + * Format: double + * @description Geographic longitude coordinate + * @example -122.31829217664796 + */ + longitude: number; + /** + * @description Current status of the site + * @example active + * @enum {string} + */ + status: SiteStatus; + /** + * @description Physical address of the site + * @example 5740 Martin Luther King Jr Way S, Seattle, WA 98118 + */ + address: string; + /** @description Array of cell identifiers associated with the site */ + cell_id: string[]; + /** + * @description Optional color identifier for the site in hex code + * @example #FF5733 + */ + color?: string; + /** @description Optional geographical boundary coordinates defining the site perimeter as [latitude, longitude] pairs */ + boundary?: [number, number][]; + }; + /** @example { + * "Filipino Community Center": { + * "ping": 115.28, + * "download_speed": 7.16, + * "upload_speed": 8.63, + * "dbm": -78.4 + * } + * } */ + SitesSummary: { + [key: string]: { /** - * Upload and process measurement data - * @description Parses CSV data and stores it as both signal and measurement records. - * If a group is specified, any existing data with that group will be removed first. - * The CSV should include columns for date, time, coordinate, cell_id, dbm, ping, download_speed, and upload_speed. - * + * Format: double + * @description Average ping in milliseconds + * @example 137.41 */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UploadDataRequest"]; - }; - }; - responses: { - /** @description Successfully uploaded and processed data */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Bad request - missing CSV data or incorrect format */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server error during processing */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Service unavailable - database operation failed */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/secure/get-users": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; + ping: number; /** - * Get lists of registered and pending users - * @description Returns two lists: - * 1. Registered users sorted by issue date (newest first) - * 2. Pending users whose issue date is within the expiry display limit, sorted by issue date (newest first) - * + * Format: double + * @description Average download speed + * @example 5.23 */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully retrieved user lists */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GetUserResponse"]; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/secure/toggle-users": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; + download_speed: number; /** - * Toggle a user's enabled status - * @description Enables or disables a user account by their identity + * Format: double + * @description Average upload speed + * @example 7.28 */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["ToggleUserRequest"]; - }; - }; - responses: { - /** @description Successfully toggled user status */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Bad request - missing or invalid parameters */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Service unavailable - database operation failed */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/secure/login": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; + upload_speed: number; /** - * LDAP authentication - * @description Authenticates a user against an LDAP directory server. - * Uses Passport LDAP strategy which binds to the LDAP server with the provided credentials. - * On success, creates a session and redirects to /api/success. - * On failure, redirects to /api/failure. - * + * Format: double + * @description Average signal strength in dBm + * @example -85.3 */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["LdapCredentials"]; - "application/x-www-form-urlencoded": components["schemas"]["LdapCredentials"]; - }; - }; - responses: { - /** @description Successful authentication */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @example success */ - result: string; - }; - }; - }; - /** @description Authentication failed */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @example Invalid credentials */ - error: string; - }; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @example Failed to establish session */ - error: string; - }; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + dbm: number; + }; }; - "/api/logout": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; + MarkerData: { + /** + * Format: double + * @description Geographic latitude coordinate + * @example 47.681932654395915 + */ + latitude: number; + /** + * Format: double + * @description Geographic longitude coordinate + * @example -122.31829217664796 + */ + longitude: number; + /** + * @description Identifier for the device that collected the data + * @example 1e683a49d71ffd0 + */ + device_id: string; + /** + * @description Name of the site + * @example Filipino Community Center + */ + site: string; + /** + * Format: double + * @description Signal strength in dBm (optional) + * @example -78.4 + */ + dbm?: number; + /** + * Format: double + * @description Upload speed measurement + * @example 7.28 + */ + upload_speed: number; + /** + * Format: double + * @description Download speed measurement + * @example 5.23 + */ + download_speed: number; + /** + * Format: double + * @description Network latency measurement + * @example 137.41 + */ + ping: number; + /** + * @description Measurement identifier + * @example 614157263c28e1a473ede843 + */ + mid: string; + }; + LineSummaryItem: { + /** + * @description Name of the site + * @example Filipino Community Center + */ + site: string; + /** @description Time series data points for the site */ + values: { /** - * Log out user - * @description Ends the user's authenticated session + * Format: date-time + * @description Timestamp for the data point + * @example 2021-01-25T18:00:00.000Z */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successfully logged out */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/secure/edit_sites": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; + date: string; /** - * Update site configuration - * @description Updates the sites configuration file with provided data. - * Requires user to be authenticated - will redirect to login page if not authenticated. - * + * Format: double + * @description Average value for the metric at this timestamp + * @example 7.28 */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/x-www-form-urlencoded": { - /** - * @description JSON string containing site configuration data - * @example {"site1":{"name":"Site 1","location":"Building A"},"site2":{"name":"Site 2","location":"Building B"}} - */ - sites: string; - }; - }; - }; - responses: { - /** @description Site configuration successfully updated */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Bad request - invalid JSON format or missing required fields */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Unauthorized - User not logged in */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Server error while updating configuration file */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/secure/new-user": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Create a new user with cryptographic identity - * @description Creates a new user with a cryptographically secure identity using EC keys. - * Generates keypairs, creates signatures, and stores user information. - * Requires authentication - will redirect to login page if not authenticated. - * - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["NewUserRequest"]; - }; - }; - responses: { - /** @description User successfully created with cryptographic identity */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["NewUserRequest"]; - }; - }; - /** @description Unauthorized - User not logged in */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Cryptographic operation error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - /** @description Database operation error */ - 503: { - headers: { - [name: string]: unknown; - }; - content: { - "text/plain": string; - }; - }; - }; - }; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + value: number; + }[]; }; -} -export type webhooks = Record; -export interface components { - schemas: { - NewUserRequest: { - /** - * Format: email - * @description User's email address - * @example user@example.com - */ - email?: string; - /** - * @description User's first name - * @example John - */ - firstName?: string; - /** - * @description User's last name - * @example Doe - */ - lastName?: string; - }; - LdapCredentials: { - /** - * @description LDAP username (could be DN, uid, or email depending on LDAP configuration) - * @example uid=jsmith,ou=users,dc=example,dc=com - */ - username: string; - /** - * Format: password - * @description LDAP password - * @example password123 - */ - password: string; - }; - CryptoIdentityResponse: { - /** - * @description Cryptographic signature in hexadecimal format - * @example 30450221009d41a9afd... - */ - sigma_t?: string; - /** - * @description Private key in DER format converted to hexadecimal - * @example 308184020100301006... - */ - sk_t?: string; - /** - * @description Public key in DER format converted to hexadecimal - * @example 3056301006072a8648... - */ - pk_a?: string; - }; - UploadDataRequest: { - /** - * @description CSV data to be parsed and stored - * @example date,time,coordinate,cell_id,dbm,ping,download_speed,upload_speed - * 2021-01-25,18:43:54,47.681932,-122.318292,cell-1,-85.3,-87.1,137.4,5.2,7.3 - */ - csv: string; - /** - * @description Optional group identifier to associate with uploaded data - * @example fieldtrip-2021 - */ - group?: string; - }; - DeleteGroupRequest: { - /** - * @description Group identifier to delete - * @example fieldtrip-2021 - */ - group: string; - }; - /** @description List of unique group identifiers across signal and measurement data */ - GroupList: string[]; - User: { - /** - * @description Unique identifier for the user - * @example 9a8b7c6d5e4f3g2h1i - */ - identity: string; - /** - * Format: email - * @description User's email address - * @example user@example.com - */ - email: string; - /** - * @description User's first name - * @example John - */ - firstName: string; - /** - * @description User's last name - * @example Doe - */ - lastName: string; - /** - * @description Whether the user has completed registration - * @example true - */ - registered: boolean; - /** - * Format: date-time - * @description Date when the user was issued or account was created - * @example 2023-03-15T14:30:45.123Z - */ - issueDate: string; - /** - * @description Whether the user account is currently enabled - * @example true - */ - isEnabled: boolean; - /** - * @description User's public key - * @example 308201a2300d06092a864886f70d01010105000382018f003082018a02820181... - */ - publicKey: string; - /** - * @description QR code data for user registration - * @example {"sigma_t":"...","sk_t":"...","pk_a":"..."} - */ - qrCode: string; - /** - * Format: date-time - * @description Last time the user was online in ISO 8601 date-time (YYYY-MM-DDTHH:mm:ss.sssZ) - * @example 2023-03-15T14:30:45.123Z - */ - lastOnline: string; - }; - GetUserResponse: { - /** @description List of pending users (not yet registered) whose issue date is within the expiry limit */ - pending: components["schemas"]["User"][]; - /** @description List of registered users */ - registered: components["schemas"]["User"][]; - }; - ToggleUserRequest: { - /** - * @description Unique identifier of the user to update - * @example 9a8b7c6d5e4f3g2h1i - */ - identity: string; - /** - * @description New enabled status for the user - * @example true - */ - enabled: boolean; - }; - BaseMeasureDataModel: { - /** - * Format: double - * @description Geographic latitude coordinate - */ - latitude: number; - /** - * Format: double - * @description Geographic longitude coordinate - */ - longitude: number; - /** @description When the measurement was taken */ - timestamp: string; - /** @description Identifier for the cell tower or access point */ - cell_id: string; - /** @description Unique identifier for the reporting device */ - device_id: string; - /** @description Flag indicating if the measurement has been reported and will be shown on the map. */ - show_data: boolean; - }; - SignalStrengthReportModel: components["schemas"]["BaseMeasureDataModel"] & { - /** @description Signal strength in decibel-milliwatts */ - dbm: number; - /** @description Code representing the signal strength level */ - level_code: number; - }; - ConnectivityReportModel: components["schemas"]["BaseMeasureDataModel"] & { - /** - * Format: double - * @description Upload speed measurement - */ - upload_speed: number; - /** - * Format: double - * @description Download speed measurement - */ - download_speed: number; - /** - * Format: double - * @description Network latency in milliseconds - */ - ping: number; - /** - * Format: double - * @description Packet loss percentage - */ - package_loss: number; - }; - Site: { - /** - * @description Name of the site - * @example Filipino Community Center - */ - name: string; - /** - * Format: double - * @description Geographic latitude coordinate - * @example 47.681932654395915 - */ - latitude: number; - /** - * Format: double - * @description Geographic longitude coordinate - * @example -122.31829217664796 - */ - longitude: number; - /** - * @description Current status of the site - * @example active - * @enum {string} - */ - status: SiteStatus; - /** - * @description Physical address of the site - * @example 5740 Martin Luther King Jr Way S, Seattle, WA 98118 - */ - address: string; - /** @description Array of cell identifiers associated with the site */ - cell_id: string[]; - /** - * @description Optional color identifier for the site in hex code - * @example #FF5733 - */ - color?: string; - /** @description Optional geographical boundary coordinates defining the site perimeter as [latitude, longitude] pairs */ - boundary?: [ - number, - number - ][]; - }; - /** @example { - * "Filipino Community Center": { - * "ping": 115.28, - * "download_speed": 7.16, - * "upload_speed": 8.63, - * "dbm": -78.4 - * } - * } */ - SitesSummary: { - [key: string]: { - /** - * Format: double - * @description Average ping in milliseconds - * @example 137.41 - */ - ping: number; - /** - * Format: double - * @description Average download speed - * @example 5.23 - */ - download_speed: number; - /** - * Format: double - * @description Average upload speed - * @example 7.28 - */ - upload_speed: number; - /** - * Format: double - * @description Average signal strength in dBm - * @example -85.3 - */ - dbm: number; - }; - }; - MarkerData: { - /** - * Format: double - * @description Geographic latitude coordinate - * @example 47.681932654395915 - */ - latitude: number; - /** - * Format: double - * @description Geographic longitude coordinate - * @example -122.31829217664796 - */ - longitude: number; - /** - * @description Identifier for the device that collected the data - * @example 1e683a49d71ffd0 - */ - device_id: string; - /** - * @description Name of the site - * @example Filipino Community Center - */ - site: string; - /** - * Format: double - * @description Signal strength in dBm (optional) - * @example -78.4 - */ - dbm?: number; - /** - * Format: double - * @description Upload speed measurement - * @example 7.28 - */ - upload_speed: number; - /** - * Format: double - * @description Download speed measurement - * @example 5.23 - */ - download_speed: number; - /** - * Format: double - * @description Network latency measurement - * @example 137.41 - */ - ping: number; - /** - * @description Measurement identifier - * @example 614157263c28e1a473ede843 - */ - mid: string; - }; - LineSummaryItem: { - /** - * @description Name of the site - * @example Filipino Community Center - */ - site: string; - /** @description Time series data points for the site */ - values: { - /** - * Format: date-time - * @description Timestamp for the data point - * @example 2021-01-25T18:00:00.000Z - */ - date: string; - /** - * Format: double - * @description Average value for the metric at this timestamp - * @example 7.28 - */ - value: number; - }[]; - }; - UserRegistration: { - /** - * @description Public key in hexadecimal format, ed25519, pem, pkcs8. - * @example aabbccddeeff00112233445566778899 - */ - publicKey: string; - /** - * Format: byte - * @description Message to be registered in binary format. - * @example SGVsbG8gd29ybGQ= - */ - message: string; - /** - * @description Signature of message in hex. - * @example c3lzdGVtZXN0cmluZw== - */ - sigMessage: string; - }; - DataReport: { - /** @description The parameter `h_pkr` */ - h_pkr: string; - /** @description The parameter `sigma_m` */ - sigma_m: string; - /** @description The parameter `M` */ - M: string; - }; - SiteMeasurementData: { - /** - * @description Unique identifier for the record - * @example 614157263c28e1a473ede843 - */ - _id: string; - /** - * Format: double - * @description Geographic latitude coordinate - * @example 47.681932654395915 - */ - latitude: number; - /** - * Format: double - * @description Geographic longitude coordinate - * @example -122.31829217664796 - */ - longitude: number; - /** - * Format: date-time - * @description Time when the measurement was recorded - * @example 2021-01-25T18:43:54.370Z - */ - timestamp: string; - /** - * Format: double - * @description Upload speed measurement - * @example 7.289173724717997 - */ - upload_speed: number; - /** - * Format: double - * @description Download speed measurement - * @example 5.234371563131994 - */ - download_speed: number; - /** - * Format: double - * @description Amount of data transferred since the previous report - * @example 735.2343217314725 - */ - data_since_last_report: number; - /** - * Format: double - * @description Network latency measurement - * @example 137.41470114174285 - */ - ping: number; - /** - * @description Identifier for the cell/location where data was collected - * @example Filipino Community Center - */ - cell_id: string; - /** - * @description Identifier for the device that collected the data - * @example 1e683a49d71ffd0 - */ - device_id: string; - }; - DataRangeResponse: { - /** - * @description Center coordinates [latitude, longitude] of the data range - * @example [ - * 47.6062, - * -122.3321 - * ] - */ - center: number[]; - /** - * Format: double - * @description Minimum latitude value in the data range - * @example 47.5001 - */ - minLat: number; - /** - * Format: double - * @description Minimum longitude value in the data range - * @example -122.4382 - */ - minLon: number; - /** - * Format: double - * @description Maximum latitude value in the data range - * @example 47.734 - */ - maxLat: number; - /** - * Format: double - * @description Maximum longitude value in the data range - * @example -122.2364 - */ - maxLon: number; - }; - /** @description Represents a single data point in a query result */ - QueryData: { - /** - * @description The bin/bucket identifier for data categorization - * @example 3 - */ - bin: number; - /** - * @description The string representation of calculated average value for data in this bin with 2 decimal places. - * @example 45.70 - */ - average: string; - }; + UserRegistration: { + /** + * @description Public key in hexadecimal format, ed25519, pem, pkcs8. + * @example aabbccddeeff00112233445566778899 + */ + publicKey: string; + /** + * Format: byte + * @description Message to be registered in binary format. + * @example SGVsbG8gd29ybGQ= + */ + message: string; + /** + * @description Signature of message in hex. + * @example c3lzdGVtZXN0cmluZw== + */ + sigMessage: string; }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + DataReport: { + /** @description The parameter `h_pkr` */ + h_pkr: string; + /** @description The parameter `sigma_m` */ + sigma_m: string; + /** @description The parameter `M` */ + M: string; + }; + SiteMeasurementData: { + /** + * @description Unique identifier for the record + * @example 614157263c28e1a473ede843 + */ + _id: string; + /** + * Format: double + * @description Geographic latitude coordinate + * @example 47.681932654395915 + */ + latitude: number; + /** + * Format: double + * @description Geographic longitude coordinate + * @example -122.31829217664796 + */ + longitude: number; + /** + * Format: date-time + * @description Time when the measurement was recorded + * @example 2021-01-25T18:43:54.370Z + */ + timestamp: string; + /** + * Format: double + * @description Upload speed measurement + * @example 7.289173724717997 + */ + upload_speed: number; + /** + * Format: double + * @description Download speed measurement + * @example 5.234371563131994 + */ + download_speed: number; + /** + * Format: double + * @description Amount of data transferred since the previous report + * @example 735.2343217314725 + */ + data_since_last_report: number; + /** + * Format: double + * @description Network latency measurement + * @example 137.41470114174285 + */ + ping: number; + /** + * @description Identifier for the cell/location where data was collected + * @example Filipino Community Center + */ + cell_id: string; + /** + * @description Identifier for the device that collected the data + * @example 1e683a49d71ffd0 + */ + device_id: string; + }; + DataRangeResponse: { + /** + * @description Center coordinates [latitude, longitude] of the data range + * @example [ + * 47.6062, + * -122.3321 + * ] + */ + center: number[]; + /** + * Format: double + * @description Minimum latitude value in the data range + * @example 47.5001 + */ + minLat: number; + /** + * Format: double + * @description Minimum longitude value in the data range + * @example -122.4382 + */ + minLon: number; + /** + * Format: double + * @description Maximum latitude value in the data range + * @example 47.734 + */ + maxLat: number; + /** + * Format: double + * @description Maximum longitude value in the data range + * @example -122.2364 + */ + maxLon: number; + }; + /** @description Represents a single data point in a query result */ + QueryData: { + /** + * @description The bin/bucket identifier for data categorization + * @example 3 + */ + bin: number; + /** + * @description The string representation of calculated average value for data in this bin with 2 decimal places. + * @example 45.70 + */ + average: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; export interface operations { - registerUser: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UserRegistration"]; - }; - }; - responses: { - /** @description User already registered */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description User registered successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid input */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized registration */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Registration keys are invalid */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Registration period expired */ - 408: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Internal server error */ - 503: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; + registerUser: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - reportSignal: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["DataReport"]; - }; - }; - responses: { - /** @description Signal reported successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description User not found */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid signature */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; + requestBody: { + content: { + 'application/json': components['schemas']['UserRegistration']; + }; }; - reportMeasurement: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["DataReport"]; - }; - }; - responses: { - /** @description Signal reported successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description User not found */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid signature */ - 403: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; + responses: { + /** @description User already registered */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description User registered successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid input */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized registration */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Registration keys are invalid */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Registration period expired */ + 408: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - getSites: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of sites */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["Site"][]; - }; - }; + }; + reportSignal: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['DataReport']; + }; + }; + responses: { + /** @description Signal reported successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description User not found */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid signature */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + reportMeasurement: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['DataReport']; + }; + }; + responses: { + /** @description Signal reported successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description User not found */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid signature */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getSites: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of sites */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Site'][]; }; + }; }; + }; } export enum SiteStatus { - active = "active", - confirmed = "confirmed", - in_conversation = "in-conversation" + active = 'active', + confirmed = 'confirmed', + in_conversation = 'in-conversation', } From 0af04c9291a97c8a82baa189126f751c14520d7d Mon Sep 17 00:00:00 2001 From: Ananya Date: Thu, 17 Jul 2025 00:05:42 -0700 Subject: [PATCH 08/24] fix: address PR review comments Update public sites endpoint name to /api/sites --- src/routes/public-sites.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/public-sites.ts b/src/routes/public-sites.ts index 3bb04fd..9c1d216 100644 --- a/src/routes/public-sites.ts +++ b/src/routes/public-sites.ts @@ -3,7 +3,7 @@ import { Site } from '../models/site'; const router = express.Router(); -router.get('/api/public-sites', async (req: Request, res: Response) => { +router.get('/api/sites', async (req: Request, res: Response) => { try { const sites = await Site.find(); From 5ce36c1173b30c43a4007875e68d09cb2a6f4958 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Thu, 31 Jul 2025 22:20:26 -0700 Subject: [PATCH 09/24] refactor: change sites to public-sites to not confuse with old endpoint for now --- src/routes/public-sites.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/public-sites.ts b/src/routes/public-sites.ts index 9c1d216..3bb04fd 100644 --- a/src/routes/public-sites.ts +++ b/src/routes/public-sites.ts @@ -3,7 +3,7 @@ import { Site } from '../models/site'; const router = express.Router(); -router.get('/api/sites', async (req: Request, res: Response) => { +router.get('/api/public-sites', async (req: Request, res: Response) => { try { const sites = await Site.find(); From 56f13c20bb51155ecadf59fa4fdb9624f91db1ff Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Wed, 6 Aug 2025 18:27:42 -0700 Subject: [PATCH 10/24] feat: add validation for latitude and longitude --- src/models/site.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/models/site.ts b/src/models/site.ts index 1a108d8..a4cc6d3 100644 --- a/src/models/site.ts +++ b/src/models/site.ts @@ -18,10 +18,22 @@ const siteSchema = new mongoose.Schema( latitude: { type: Number, required: true, + validate: { + validator: function (latitude: number) { + return latitude >= -90 && latitude <= 90; + }, + message: 'Latitude must be between -90 and 90 degrees', + }, }, longitude: { type: Number, required: true, + validate: { + validator: function (longitude: number) { + return longitude >= -180 && longitude <= 180; + }, + message: 'Longitude must be between -180 and 180 degrees', + }, }, status: { type: String, From 500adf2185f163a086509fd7c71ec29d9ec1e5e8 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Wed, 6 Aug 2025 20:17:21 -0700 Subject: [PATCH 11/24] docs: add comments to secure-site route handlers --- src/routes/public-sites.ts | 1 + src/routes/secure-site.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/routes/public-sites.ts b/src/routes/public-sites.ts index 3bb04fd..3055007 100644 --- a/src/routes/public-sites.ts +++ b/src/routes/public-sites.ts @@ -3,6 +3,7 @@ import { Site } from '../models/site'; const router = express.Router(); +// Get list of all sites from database router.get('/api/public-sites', async (req: Request, res: Response) => { try { const sites = await Site.find(); diff --git a/src/routes/secure-site.ts b/src/routes/secure-site.ts index 572c5ff..df306d9 100644 --- a/src/routes/secure-site.ts +++ b/src/routes/secure-site.ts @@ -7,6 +7,7 @@ const router = express.Router(); type SiteRequest = components['schemas']['Site']; +// Edit an existing site router.put( '/api/secure-site', // connectEnsureLogin.ensureLoggedIn('/api/failure'), @@ -37,6 +38,7 @@ router.put( }, ); +// Create a new site router.post( '/api/secure-site', // connectEnsureLogin.ensureLoggedIn('/api/failure'), @@ -60,6 +62,7 @@ router.post( }, ); +// Delete an existing site router.delete( '/api/secure-site', // connectEnsureLogin.ensureLoggedIn('/api/failure'), From 2f272819d5b6df6990a6cf9cb9481ea97ba1d3e1 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Sat, 9 Aug 2025 16:31:04 -0700 Subject: [PATCH 12/24] refactor: replace api/public-sites with api/sites and make necessary changes to ensure compatability --- src/models/measurement.ts | 1 - src/models/user.ts | 1 - src/routes/public-sites.ts | 6 +- src/routes/query.ts | 2 +- src/routes/report.ts | 2 +- src/types/schema.d.ts | 431 ++++++++++++++++++------------------- 6 files changed, 208 insertions(+), 235 deletions(-) diff --git a/src/models/measurement.ts b/src/models/measurement.ts index bb47c22..a9f910a 100644 --- a/src/models/measurement.ts +++ b/src/models/measurement.ts @@ -46,5 +46,4 @@ const MeasurementData = mongoose.model< MeasurementModelInterface >('Measurement', measurementSchema); - export { MeasurementData, IMeasurement, MeasurementDoc }; diff --git a/src/models/user.ts b/src/models/user.ts index a86c5f9..eca0349 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -65,5 +65,4 @@ userSchema.statics.build = (attr: IUser) => { const User = mongoose.model('User', userSchema); - export { User, IUser, UserDoc }; diff --git a/src/routes/public-sites.ts b/src/routes/public-sites.ts index 3055007..839cb7a 100644 --- a/src/routes/public-sites.ts +++ b/src/routes/public-sites.ts @@ -4,13 +4,11 @@ import { Site } from '../models/site'; const router = express.Router(); // Get list of all sites from database -router.get('/api/public-sites', async (req: Request, res: Response) => { +router.get('/api/sites', async (req: Request, res: Response) => { try { const sites = await Site.find(); - res.status(200).json({ - sites: sites, - }); + res.status(200).json(sites); } catch (error) { console.error('Error retrieving public sites:', error); res.status(500).send('Internal server error'); diff --git a/src/routes/query.ts b/src/routes/query.ts index 708ec4c..da0a193 100644 --- a/src/routes/query.ts +++ b/src/routes/query.ts @@ -67,7 +67,7 @@ function updateSite() { return JSON.parse(fs.readFileSync(defaultModelJsonPath).toString()); } } -router.get('/api/sites', (_, res: Response) => { +router.get('/api/old-sites', (_, res: Response) => { sites = updateSite(); res.send(sites); }); diff --git a/src/routes/report.ts b/src/routes/report.ts index f153a00..8c522f9 100644 --- a/src/routes/report.ts +++ b/src/routes/report.ts @@ -11,7 +11,7 @@ function runIfAuthenticated(req: Request, res: Response, next: any) { const signature = Buffer.from(req.body.sigma_m, 'hex'); const message = Buffer.from(req.body.M, 'hex'); User.findOne({ identity: req.body.h_pkr.toLowerCase() }) - .then((user) => { + .then(user => { if (!user) { res.status(401).send('user not found'); logger.debug('User not found'); diff --git a/src/types/schema.d.ts b/src/types/schema.d.ts index f04c367..83174b5 100644 --- a/src/types/schema.d.ts +++ b/src/types/schema.d.ts @@ -415,7 +415,7 @@ export interface paths { patch?: never; trace?: never; }; - '/api/sites': { + '/api/old-sites': { parameters: { query?: never; header?: never; @@ -426,7 +426,7 @@ export interface paths { * Get all sites * @description Returns a list of all available sites with their location and status information */ - get: operations['getSites']; + get: operations['getSitesOld']; put?: never; post?: never; delete?: never; @@ -435,197 +435,7 @@ export interface paths { patch?: never; trace?: never; }; - '/api/secure-site': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** - * Edit an existing site - * @description Updates an existing site with the provided information - */ - put: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['Site']; - }; - }; - responses: { - /** @description Site successfully updated */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Site']; - }; - }; - /** @description Bad request - invalid site data */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Site not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - /** - * Add a new site - * @description Creates a new site with the provided information - */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['Site']; - }; - }; - responses: { - /** @description Site successfully created */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Site']; - }; - }; - /** @description Bad request - invalid site data */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - /** - * Delete a site - * @description Removes an existing site from the system - */ - delete: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['Site']; - }; - }; - responses: { - /** @description Site successfully deleted */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Bad request - invalid site data */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Unauthorized - redirects to /api/failure */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Site not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/public-sites': { + '/api/sites': { parameters: { query?: never; header?: never; @@ -633,41 +443,10 @@ export interface paths { cookie?: never; }; /** - * Get sites list - * @description Returns a list of sites + * Get all sites + * @description Returns a list of all available sites with their location and status information */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of public */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @description List of sites */ - sites: components['schemas']['Site'][]; - }; - }; - }; - /** @description Server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/plain': string; - }; - }; - }; - }; + get: operations['getSites']; put?: never; post?: never; delete?: never; @@ -1316,6 +1095,184 @@ export interface paths { patch?: never; trace?: never; }; + '/api/secure-site': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Update an existing site + * @description Updates an existing site with the provided information + */ + put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Site']; + }; + }; + responses: { + /** @description Site successfully updated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Bad request - invalid site data */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Unauthorized - User not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Server error while updating site */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + /** + * Add a new site + * @description Creates a new site with the provided information + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Site']; + }; + }; + responses: { + /** @description Site successfully created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Bad request - invalid site data */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Unauthorized - User not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Server error while creating site */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + /** + * Delete a site + * @description Removes an existing site from the system + */ + delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Site']; + }; + }; + responses: { + /** @description Site successfully deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Bad request - invalid site data */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Unauthorized - User not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Server error while deleting site */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -1952,6 +1909,26 @@ export interface operations { }; }; }; + getSitesOld: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of sites */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Site'][]; + }; + }; + }; + }; getSites: { parameters: { query?: never; From c78960d32d82704e894dab25fa496bab52a3febe Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Fri, 15 Aug 2025 17:55:43 -0700 Subject: [PATCH 13/24] feat: add testing coverage for secure-site.ts functions - Create secure-site.test.ts to test all the functions in secure-site.ts with exhaustive cases - Update package.json to include an "npm run test" script to run tests - Change secure-site.ts to use event handlers for ease of testing --- package-lock.json | 714 ++++++++++++++++++++++++++++++++++- package.json | 7 +- src/routes/secure-site.ts | 136 ++++--- src/test/secure-site.test.ts | 155 ++++++++ 4 files changed, 938 insertions(+), 74 deletions(-) create mode 100644 src/test/secure-site.test.ts diff --git a/package-lock.json b/package-lock.json index 39612f7..9730b24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,12 +38,16 @@ "@types/express": "^5.0.0", "@types/express-session": "^1.18.1", "@types/leaflet": "^1.7.5", + "@types/mocha": "^10.0.10", "@types/passport": "^1.0.17", "copyfiles": "^2.4.1", "js-yaml": "^4.1.0", "jsdom-global": "3.0.2", + "mocha": "^11.7.1", + "node-mocks-http": "^1.17.2", "prettier": "^3.5.3", "semantic-release": "^24.2.4", + "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0" } }, @@ -219,6 +223,102 @@ "node": ">=18" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -529,6 +629,16 @@ "@octokit/openapi-types": "^25.0.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -1559,6 +1669,12 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "license": "MIT" }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true + }, "node_modules/@types/node": { "version": "22.13.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz", @@ -1947,6 +2063,12 @@ "node": ">=8" } }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "node_modules/bson": { "version": "6.10.3", "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", @@ -2011,6 +2133,18 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -2487,6 +2621,18 @@ "ms": "2.0.0" } }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/decimal.js": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", @@ -2641,6 +2787,12 @@ "xtend": "^4.0.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3181,6 +3333,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -3201,6 +3362,22 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", @@ -3601,6 +3778,15 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -4085,6 +4271,21 @@ "node": "^18.17 || >=20.6.1" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/java-properties": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", @@ -4356,6 +4557,50 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -4572,6 +4817,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -4585,6 +4839,295 @@ "node": ">=10" } }, + "node_modules/mocha": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.1.tgz", + "integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==", + "dev": true, + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/mongodb": { "version": "6.13.1", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.1.tgz", @@ -4770,6 +5313,48 @@ "node": ">=18" } }, + "node_modules/node-mocks-http": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.17.2.tgz", + "integrity": "sha512-HVxSnjNzE9NzoWMx9T9z4MLqwMpLwVvA0oVZ+L+gXskYXEJ6tFn3Kx4LargoB6ie7ZlCLplv7QbWO6N+MysWGA==", + "dev": true, + "dependencies": { + "accepts": "^1.3.7", + "content-disposition": "^0.5.3", + "depd": "^1.1.0", + "fresh": "^0.5.2", + "merge-descriptors": "^1.0.1", + "methods": "^1.1.2", + "mime": "^1.3.4", + "parseurl": "^1.3.3", + "range-parser": "^1.2.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@types/express": "^4.17.21 || ^5.0.0", + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + }, + "@types/node": { + "optional": true + } + } + }, + "node_modules/node-mocks-http/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-rsa": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", @@ -7816,6 +8401,12 @@ "node": ">=4" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7974,6 +8565,22 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -8217,6 +8824,15 @@ "node": ">= 0.8" } }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -8767,6 +9383,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -9213,6 +9838,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -9226,6 +9866,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -9571,7 +10224,6 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, - "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -9960,6 +10612,12 @@ "dev": true, "license": "MIT" }, + "node_modules/workerpool": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.3.tgz", + "integrity": "sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==", + "dev": true + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -9978,6 +10636,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -10070,6 +10746,30 @@ "node": ">=10" } }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -10080,6 +10780,18 @@ "node": ">=6" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", diff --git a/package.json b/package.json index 7d2b25c..7fa6532 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,16 @@ "@types/express": "^5.0.0", "@types/express-session": "^1.18.1", "@types/leaflet": "^1.7.5", + "@types/mocha": "^10.0.10", "@types/passport": "^1.0.17", "copyfiles": "^2.4.1", "js-yaml": "^4.1.0", "jsdom-global": "3.0.2", + "mocha": "^11.7.1", + "node-mocks-http": "^1.17.2", "prettier": "^3.5.3", "semantic-release": "^24.2.4", + "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0" }, "dependencies": { @@ -46,6 +50,7 @@ "build": "tsc -p tsconfig.json && copyfiles -f ./src/models/*.json ./build/src/models", "start-built": "node ./build/src/index.js", "format": "prettier --write \"src/**/*.{ts,tsx,json}\"", - "format:check": "prettier --check \"src/**/*.{ts,tsx,json}\"" + "format:check": "prettier --check \"src/**/*.{ts,tsx,json}\"", + "test": "mocha -r ts-node/register 'src/test/**/*.test.ts'" } } diff --git a/src/routes/secure-site.ts b/src/routes/secure-site.ts index df306d9..c213286 100644 --- a/src/routes/secure-site.ts +++ b/src/routes/secure-site.ts @@ -8,87 +8,79 @@ const router = express.Router(); type SiteRequest = components['schemas']['Site']; // Edit an existing site -router.put( - '/api/secure-site', - // connectEnsureLogin.ensureLoggedIn('/api/failure'), - async (req: Request, res: Response) => { - try { - const siteData: SiteRequest = req.body; - - if (!siteData.name) { - res.status(400).json({ error: 'Site name is required' }); - return; - } - - const updatedSite = await Site.findOneAndUpdate( - { name: siteData.name }, - siteData, - { new: true, runValidators: true }, - ); - - if (!updatedSite) { - res.status(404).json({ error: 'Site not found' }); - return; - } - - res.status(200).json({ message: 'Site updated successfully' }); - } catch (error: any) { - res.status(500).json({ error: 'Internal server error' }); +export const putSecureSite = async (req: Request, res: Response) => { + try { + const siteData: SiteRequest = req.body; + + if (!siteData.name) { + res.status(400).json({ error: 'Site name is required' }); + return; + } + + const updatedSite = await Site.findOneAndUpdate( + { name: siteData.name }, + siteData, + { new: true, runValidators: true }, + ); + + if (!updatedSite) { + res.status(404).json({ error: 'Site not found' }); + return; } - }, -); + + res.status(200).json({ message: 'Site updated successfully' }); + } catch (error: any) { + res.status(500).json({ error: 'Internal server error' }); + } +}; // Create a new site -router.post( - '/api/secure-site', - // connectEnsureLogin.ensureLoggedIn('/api/failure'), - async (req: Request, res: Response) => { - try { - const siteData: SiteRequest = req.body; - - const newSite = Site.build(siteData); - const savedSite = await newSite.save(); - - res.status(201).json(savedSite); - } catch (error: any) { - if (error.name === 'ValidationError') { - res - .status(400) - .json({ error: 'Validation error', details: error.message }); - return; - } - res.status(500).json({ error: 'Internal server error' }); +export const postSecureSite = async (req: Request, res: Response) => { + try { + const siteData: SiteRequest = req.body; + + const newSite = Site.build(siteData); + const savedSite = await newSite.save(); + + res.status(201).json({ message: 'Site created successfully' }); + } catch (error: any) { + if (error.name === 'ValidationError') { + res + .status(400) + .json({ error: 'Validation error', details: error.message }); + return; } - }, -); + res.status(500).json({ error: 'Internal server error' }); + } +}; // Delete an existing site -router.delete( - '/api/secure-site', - // connectEnsureLogin.ensureLoggedIn('/api/failure'), - async (req: Request, res: Response) => { - try { - const siteData: SiteRequest = req.body; - - if (!siteData.name) { - res.status(400).json({ error: 'Site name is required' }); - return; - } +export const deleteSecureSite = async (req: Request, res: Response) => { + try { + const siteData: SiteRequest = req.body; - const deletedSite = await Site.findOneAndDelete({ name: siteData.name }); + if (!siteData.name) { + res.status(400).json({ error: 'Site name is required' }); + return; + } - if (!deletedSite) { - res.status(404).json({ error: 'Site not found' }); - return; - } + const deletedSite = await Site.findOneAndDelete({ name: siteData.name }); - res - .status(200) - .json({ message: 'Site deleted successfully', site: deletedSite }); - } catch (error: any) { - res.status(500).json({ error: 'Internal server error' }); + if (!deletedSite) { + res.status(404).json({ error: 'Site not found' }); + return; } - }, -); + + res + .status(200) + .json({ message: 'Site deleted successfully', site: deletedSite }); + } catch (error: any) { + res.status(500).json({ error: 'Internal server error' }); + } +}; + +router.put('/api/secure-site', putSecureSite); +router.post('/api/secure-site', postSecureSite); +router.delete('/api/secure-site', deleteSecureSite); export { router as secureSitesRouter }; diff --git a/src/test/secure-site.test.ts b/src/test/secure-site.test.ts new file mode 100644 index 0000000..8483611 --- /dev/null +++ b/src/test/secure-site.test.ts @@ -0,0 +1,155 @@ +import * as assert from 'assert'; +import * as httpMocks from 'node-mocks-http'; +import { putSecureSite, postSecureSite, deleteSecureSite } from '../routes/secure-site'; +import { Site } from '../models/site'; + +describe('secure-site event handlers', function () { + + describe('PUT /api/secure-site', function () { + + it('400 if name missing', async function () { + const req = httpMocks.createRequest({ method: 'PUT', body: {} }); + const res = httpMocks.createResponse(); + + await putSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 400); + assert.deepStrictEqual(res._getJSONData(), { error: 'Site name is required' }); + }); + + it('404 if site not found', async function () { + (Site.findOneAndUpdate as any) = async () => null; + + const req = httpMocks.createRequest({ method: 'PUT', body: { name: 'X' } }); + const res = httpMocks.createResponse(); + + await putSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 404); + assert.deepStrictEqual(res._getJSONData(), { error: 'Site not found' }); + }); + + it('200 on success', async function () { + (Site.findOneAndUpdate as any) = async () => ({ name: 'X' }); + + const req = httpMocks.createRequest({ method: 'PUT', body: { name: 'X' } }); + const res = httpMocks.createResponse(); + + await putSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 200); + assert.deepStrictEqual(res._getJSONData(), { message: 'Site updated successfully' }); + }); + + it('500 on error', async function () { + (Site.findOneAndUpdate as any) = async () => { throw new Error('fail'); }; + + const req = httpMocks.createRequest({ method: 'PUT', body: { name: 'X' } }); + const res = httpMocks.createResponse(); + + await putSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 500); + assert.deepStrictEqual(res._getJSONData(), { error: 'Internal server error' }); + }); + + }); + + describe('POST /api/secure-site', function () { + + it('201 on success', async function () { + (Site.build as any) = () => ({ save: async () => ({}) }); + + const req = httpMocks.createRequest({ method: 'POST', body: { name: 'New' } }); + const res = httpMocks.createResponse(); + + await postSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 201); + assert.deepStrictEqual(res._getJSONData(), { message: 'Site created successfully' }); + }); + + it('400 on validation error', async function () { + (Site.build as any) = () => ({ + save: async () => { throw { name: 'ValidationError', message: 'Bad' }; } + }); + + const req = httpMocks.createRequest({ method: 'POST', body: { name: 'Invalid' } }); + const res = httpMocks.createResponse(); + + await postSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 400); + assert.deepStrictEqual(res._getJSONData(), { error: 'Validation error', details: 'Bad' }); + }); + + it('500 on other error', async function () { + (Site.build as any) = () => ({ + save: async () => { throw new Error('fail'); } + }); + + const req = httpMocks.createRequest({ method: 'POST', body: { name: 'X' } }); + const res = httpMocks.createResponse(); + + await postSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 500); + assert.deepStrictEqual(res._getJSONData(), { error: 'Internal server error' }); + }); + + }); + + describe('DELETE /api/secure-site', function () { + + it('400 if name missing', async function () { + const req = httpMocks.createRequest({ method: 'DELETE', body: {} }); + const res = httpMocks.createResponse(); + + await deleteSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 400); + assert.deepStrictEqual(res._getJSONData(), { error: 'Site name is required' }); + }); + + it('404 if site not found', async function () { + (Site.findOneAndDelete as any) = async () => null; + + const req = httpMocks.createRequest({ method: 'DELETE', body: { name: 'X' } }); + const res = httpMocks.createResponse(); + + await deleteSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 404); + assert.deepStrictEqual(res._getJSONData(), { error: 'Site not found' }); + }); + + it('200 on success', async function () { + (Site.findOneAndDelete as any) = async () => ({ name: 'X' }); + + const req = httpMocks.createRequest({ method: 'DELETE', body: { name: 'X' } }); + const res = httpMocks.createResponse(); + + await deleteSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 200); + assert.deepStrictEqual(res._getJSONData(), { + message: 'Site deleted successfully', + site: { name: 'X' } + }); + }); + + it('500 on error', async function () { + (Site.findOneAndDelete as any) = async () => { throw new Error('fail'); }; + + const req = httpMocks.createRequest({ method: 'DELETE', body: { name: 'X' } }); + const res = httpMocks.createResponse(); + + await deleteSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 500); + assert.deepStrictEqual(res._getJSONData(), { error: 'Internal server error' }); + }); + + }); + +}); From c99f4d925fa1b4fe846f4e40325315e3e61c52a4 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Fri, 15 Aug 2025 18:04:39 -0700 Subject: [PATCH 14/24] refactor: run format --- src/test/secure-site.test.ts | 110 +++++++++++++++++++++++++---------- 1 file changed, 79 insertions(+), 31 deletions(-) diff --git a/src/test/secure-site.test.ts b/src/test/secure-site.test.ts index 8483611..9f05fdb 100644 --- a/src/test/secure-site.test.ts +++ b/src/test/secure-site.test.ts @@ -1,12 +1,14 @@ import * as assert from 'assert'; import * as httpMocks from 'node-mocks-http'; -import { putSecureSite, postSecureSite, deleteSecureSite } from '../routes/secure-site'; +import { + putSecureSite, + postSecureSite, + deleteSecureSite, +} from '../routes/secure-site'; import { Site } from '../models/site'; describe('secure-site event handlers', function () { - describe('PUT /api/secure-site', function () { - it('400 if name missing', async function () { const req = httpMocks.createRequest({ method: 'PUT', body: {} }); const res = httpMocks.createResponse(); @@ -14,13 +16,18 @@ describe('secure-site event handlers', function () { await putSecureSite(req, res); assert.strictEqual(res._getStatusCode(), 400); - assert.deepStrictEqual(res._getJSONData(), { error: 'Site name is required' }); + assert.deepStrictEqual(res._getJSONData(), { + error: 'Site name is required', + }); }); it('404 if site not found', async function () { (Site.findOneAndUpdate as any) = async () => null; - const req = httpMocks.createRequest({ method: 'PUT', body: { name: 'X' } }); + const req = httpMocks.createRequest({ + method: 'PUT', + body: { name: 'X' }, + }); const res = httpMocks.createResponse(); await putSecureSite(req, res); @@ -32,75 +39,103 @@ describe('secure-site event handlers', function () { it('200 on success', async function () { (Site.findOneAndUpdate as any) = async () => ({ name: 'X' }); - const req = httpMocks.createRequest({ method: 'PUT', body: { name: 'X' } }); + const req = httpMocks.createRequest({ + method: 'PUT', + body: { name: 'X' }, + }); const res = httpMocks.createResponse(); await putSecureSite(req, res); assert.strictEqual(res._getStatusCode(), 200); - assert.deepStrictEqual(res._getJSONData(), { message: 'Site updated successfully' }); + assert.deepStrictEqual(res._getJSONData(), { + message: 'Site updated successfully', + }); }); it('500 on error', async function () { - (Site.findOneAndUpdate as any) = async () => { throw new Error('fail'); }; + (Site.findOneAndUpdate as any) = async () => { + throw new Error('fail'); + }; - const req = httpMocks.createRequest({ method: 'PUT', body: { name: 'X' } }); + const req = httpMocks.createRequest({ + method: 'PUT', + body: { name: 'X' }, + }); const res = httpMocks.createResponse(); await putSecureSite(req, res); assert.strictEqual(res._getStatusCode(), 500); - assert.deepStrictEqual(res._getJSONData(), { error: 'Internal server error' }); + assert.deepStrictEqual(res._getJSONData(), { + error: 'Internal server error', + }); }); - }); describe('POST /api/secure-site', function () { - it('201 on success', async function () { (Site.build as any) = () => ({ save: async () => ({}) }); - const req = httpMocks.createRequest({ method: 'POST', body: { name: 'New' } }); + const req = httpMocks.createRequest({ + method: 'POST', + body: { name: 'New' }, + }); const res = httpMocks.createResponse(); await postSecureSite(req, res); assert.strictEqual(res._getStatusCode(), 201); - assert.deepStrictEqual(res._getJSONData(), { message: 'Site created successfully' }); + assert.deepStrictEqual(res._getJSONData(), { + message: 'Site created successfully', + }); }); it('400 on validation error', async function () { (Site.build as any) = () => ({ - save: async () => { throw { name: 'ValidationError', message: 'Bad' }; } + save: async () => { + throw { name: 'ValidationError', message: 'Bad' }; + }, }); - const req = httpMocks.createRequest({ method: 'POST', body: { name: 'Invalid' } }); + const req = httpMocks.createRequest({ + method: 'POST', + body: { name: 'Invalid' }, + }); const res = httpMocks.createResponse(); await postSecureSite(req, res); assert.strictEqual(res._getStatusCode(), 400); - assert.deepStrictEqual(res._getJSONData(), { error: 'Validation error', details: 'Bad' }); + assert.deepStrictEqual(res._getJSONData(), { + error: 'Validation error', + details: 'Bad', + }); }); it('500 on other error', async function () { (Site.build as any) = () => ({ - save: async () => { throw new Error('fail'); } + save: async () => { + throw new Error('fail'); + }, }); - const req = httpMocks.createRequest({ method: 'POST', body: { name: 'X' } }); + const req = httpMocks.createRequest({ + method: 'POST', + body: { name: 'X' }, + }); const res = httpMocks.createResponse(); await postSecureSite(req, res); assert.strictEqual(res._getStatusCode(), 500); - assert.deepStrictEqual(res._getJSONData(), { error: 'Internal server error' }); + assert.deepStrictEqual(res._getJSONData(), { + error: 'Internal server error', + }); }); - }); describe('DELETE /api/secure-site', function () { - it('400 if name missing', async function () { const req = httpMocks.createRequest({ method: 'DELETE', body: {} }); const res = httpMocks.createResponse(); @@ -108,13 +143,18 @@ describe('secure-site event handlers', function () { await deleteSecureSite(req, res); assert.strictEqual(res._getStatusCode(), 400); - assert.deepStrictEqual(res._getJSONData(), { error: 'Site name is required' }); + assert.deepStrictEqual(res._getJSONData(), { + error: 'Site name is required', + }); }); it('404 if site not found', async function () { (Site.findOneAndDelete as any) = async () => null; - const req = httpMocks.createRequest({ method: 'DELETE', body: { name: 'X' } }); + const req = httpMocks.createRequest({ + method: 'DELETE', + body: { name: 'X' }, + }); const res = httpMocks.createResponse(); await deleteSecureSite(req, res); @@ -126,7 +166,10 @@ describe('secure-site event handlers', function () { it('200 on success', async function () { (Site.findOneAndDelete as any) = async () => ({ name: 'X' }); - const req = httpMocks.createRequest({ method: 'DELETE', body: { name: 'X' } }); + const req = httpMocks.createRequest({ + method: 'DELETE', + body: { name: 'X' }, + }); const res = httpMocks.createResponse(); await deleteSecureSite(req, res); @@ -134,22 +177,27 @@ describe('secure-site event handlers', function () { assert.strictEqual(res._getStatusCode(), 200); assert.deepStrictEqual(res._getJSONData(), { message: 'Site deleted successfully', - site: { name: 'X' } + site: { name: 'X' }, }); }); it('500 on error', async function () { - (Site.findOneAndDelete as any) = async () => { throw new Error('fail'); }; + (Site.findOneAndDelete as any) = async () => { + throw new Error('fail'); + }; - const req = httpMocks.createRequest({ method: 'DELETE', body: { name: 'X' } }); + const req = httpMocks.createRequest({ + method: 'DELETE', + body: { name: 'X' }, + }); const res = httpMocks.createResponse(); await deleteSecureSite(req, res); assert.strictEqual(res._getStatusCode(), 500); - assert.deepStrictEqual(res._getJSONData(), { error: 'Internal server error' }); + assert.deepStrictEqual(res._getJSONData(), { + error: 'Internal server error', + }); }); - }); - }); From f0361e7e134d17a232ab5012a0d05333236c1916 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Fri, 15 Aug 2025 21:45:54 -0700 Subject: [PATCH 15/24] feat: add test step to CI --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 546e10d..c02d3e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,3 +36,6 @@ jobs: REPO_NAME: "ccn-coverage-docker" TARGET_ARTIFACT_NAME: "ccn-coverage-api" run: npx semantic-release + + - name: Run tests + run: npm run test From d737267e581d7975f29644a1e3ea907ee55b3014 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Fri, 15 Aug 2025 21:48:41 -0700 Subject: [PATCH 16/24] fix: make test action run before release --- .github/workflows/release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c02d3e7..60563c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,6 +25,9 @@ jobs: - name: Install dependencies run: npm install --include=dev + - name: Run tests + run: npm run test + - name: Run semantic-release env: GH_TOKEN: ${{ secrets.GH_TOKEN }} @@ -37,5 +40,4 @@ jobs: TARGET_ARTIFACT_NAME: "ccn-coverage-api" run: npx semantic-release - - name: Run tests - run: npm run test + From 488ea2e4f18ad4584184d2e88beb2adb84802dc9 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Wed, 27 Aug 2025 20:31:41 -0700 Subject: [PATCH 17/24] refactor: remove old-sites --- src/routes/query.ts | 4 ---- src/types/schema.d.ts | 40 ---------------------------------------- 2 files changed, 44 deletions(-) diff --git a/src/routes/query.ts b/src/routes/query.ts index da0a193..906dee3 100644 --- a/src/routes/query.ts +++ b/src/routes/query.ts @@ -67,10 +67,6 @@ function updateSite() { return JSON.parse(fs.readFileSync(defaultModelJsonPath).toString()); } } -router.get('/api/old-sites', (_, res: Response) => { - sites = updateSite(); - res.send(sites); -}); router.get('/api/dataRange', (_, res) => { res.send(dataRange); diff --git a/src/types/schema.d.ts b/src/types/schema.d.ts index 83174b5..6dbaf14 100644 --- a/src/types/schema.d.ts +++ b/src/types/schema.d.ts @@ -415,26 +415,6 @@ export interface paths { patch?: never; trace?: never; }; - '/api/old-sites': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get all sites - * @description Returns a list of all available sites with their location and status information - */ - get: operations['getSitesOld']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; '/api/sites': { parameters: { query?: never; @@ -1909,26 +1889,6 @@ export interface operations { }; }; }; - getSitesOld: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of sites */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['Site'][]; - }; - }; - }; - }; getSites: { parameters: { query?: never; From c620d8313778c6819ae9491b65bcf0ca51a11ca4 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Thu, 28 Aug 2025 12:07:00 -0700 Subject: [PATCH 18/24] fix: put edit-sites behind secure --- src/routes/secure-site.ts | 19 ++++++++++++++++--- src/types/schema.d.ts | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/routes/secure-site.ts b/src/routes/secure-site.ts index c213286..8bdd5cb 100644 --- a/src/routes/secure-site.ts +++ b/src/routes/secure-site.ts @@ -2,6 +2,7 @@ import express, { Request, Response } from 'express'; import { Site } from '../models/site'; // import connectEnsureLogin from 'connect-ensure-login'; import { components } from '../types/schema'; +import connectEnsureLogin from 'connect-ensure-login'; const router = express.Router(); @@ -79,8 +80,20 @@ export const deleteSecureSite = async (req: Request, res: Response) => { } }; -router.put('/api/secure-site', putSecureSite); -router.post('/api/secure-site', postSecureSite); -router.delete('/api/secure-site', deleteSecureSite); +router.put( + '/secure/edit-sites', + connectEnsureLogin.ensureLoggedIn(), + putSecureSite, +); +router.post( + '/secure/edit-sites', + connectEnsureLogin.ensureLoggedIn(), + postSecureSite, +); +router.delete( + '/secure/edit-sites', + connectEnsureLogin.ensureLoggedIn(), + deleteSecureSite, +); export { router as secureSitesRouter }; diff --git a/src/types/schema.d.ts b/src/types/schema.d.ts index 6dbaf14..454747e 100644 --- a/src/types/schema.d.ts +++ b/src/types/schema.d.ts @@ -1075,7 +1075,7 @@ export interface paths { patch?: never; trace?: never; }; - '/api/secure-site': { + '/secure/edit-sites': { parameters: { query?: never; header?: never; From 2a992130429721f34be822d6e81ef452db28b3fd Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Sun, 7 Sep 2025 12:28:50 -0700 Subject: [PATCH 19/24] refactor: remove duplicate line --- src/routes/secure-site.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/secure-site.ts b/src/routes/secure-site.ts index 8bdd5cb..46d5c6e 100644 --- a/src/routes/secure-site.ts +++ b/src/routes/secure-site.ts @@ -1,6 +1,5 @@ import express, { Request, Response } from 'express'; import { Site } from '../models/site'; -// import connectEnsureLogin from 'connect-ensure-login'; import { components } from '../types/schema'; import connectEnsureLogin from 'connect-ensure-login'; From ac44be1fe03fc27c198e61d8c7286a7a50dafe67 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Sun, 7 Sep 2025 12:39:21 -0700 Subject: [PATCH 20/24] refactor: remove empty line --- .github/workflows/release.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 60563c2..e37cd5e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,4 @@ jobs: REPO_OWNER: "Local-Connectivity-Lab" REPO_NAME: "ccn-coverage-docker" TARGET_ARTIFACT_NAME: "ccn-coverage-api" - run: npx semantic-release - - + run: npx semantic-release \ No newline at end of file From 6af9f545c0ce38c102d9206cf34db584877e6f03 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Sun, 26 Oct 2025 22:39:11 -0700 Subject: [PATCH 21/24] feat: create new field "identity" to uniquely identify sites --- src/models/site.ts | 4 ++ src/routes/secure-site.ts | 43 +++++++++++++++------ src/test/secure-site.test.ts | 42 +++++++++++++-------- src/types/schema.d.ts | 72 +++++++++++++++++++++++++++++++----- 4 files changed, 123 insertions(+), 38 deletions(-) diff --git a/src/models/site.ts b/src/models/site.ts index a4cc6d3..53ad7ad 100644 --- a/src/models/site.ts +++ b/src/models/site.ts @@ -11,6 +11,10 @@ interface SiteModelInterface extends mongoose.Model { const siteSchema = new mongoose.Schema( { + identity: { + type: String, + required: true, + }, name: { type: String, required: true, diff --git a/src/routes/secure-site.ts b/src/routes/secure-site.ts index 46d5c6e..1f7c7e7 100644 --- a/src/routes/secure-site.ts +++ b/src/routes/secure-site.ts @@ -2,23 +2,25 @@ import express, { Request, Response } from 'express'; import { Site } from '../models/site'; import { components } from '../types/schema'; import connectEnsureLogin from 'connect-ensure-login'; +import * as crypto from 'crypto'; const router = express.Router(); -type SiteRequest = components['schemas']['Site']; +type EditSiteRequest = components['schemas']['Site']; +type NewSiteRequest = components['schemas']['NewSiteRequest']; // Edit an existing site export const putSecureSite = async (req: Request, res: Response) => { try { - const siteData: SiteRequest = req.body; + const siteData: EditSiteRequest = req.body; - if (!siteData.name) { - res.status(400).json({ error: 'Site name is required' }); + if (!siteData.identity) { + res.status(400).json({ error: 'Site identity is required' }); return; } const updatedSite = await Site.findOneAndUpdate( - { name: siteData.name }, + { identity: siteData.identity }, siteData, { new: true, runValidators: true }, ); @@ -35,14 +37,30 @@ export const putSecureSite = async (req: Request, res: Response) => { }; // Create a new site +// Summary of changes: +// Site has all the old fields, but there is a new field called identity +// Fill that field with +// import * as Crypto from 'crypto'; +// Crypto.createHash('sha256') +// .update(skpk) +// .digest('hex'); export const postSecureSite = async (req: Request, res: Response) => { try { - const siteData: SiteRequest = req.body; + const siteData: NewSiteRequest = req.body; - const newSite = Site.build(siteData); + // Generate identity using SHA256 hash + const skpk = siteData.name; + const identity = crypto.createHash('sha256').update(skpk).digest('hex'); + + const siteWithIdentity: EditSiteRequest = { + identity, + ...siteData, + }; + + const newSite = Site.build(siteWithIdentity); const savedSite = await newSite.save(); - res.status(201).json({ message: 'Site created successfully' }); + res.status(201).json({ message: 'Site created successfully', identity }); } catch (error: any) { if (error.name === 'ValidationError') { res @@ -55,16 +73,17 @@ export const postSecureSite = async (req: Request, res: Response) => { }; // Delete an existing site +// The request should just be the string with the identity (check schema.d.ts) export const deleteSecureSite = async (req: Request, res: Response) => { try { - const siteData: SiteRequest = req.body; + const { identity } = req.body; - if (!siteData.name) { - res.status(400).json({ error: 'Site name is required' }); + if (!identity) { + res.status(400).json({ error: 'Site identity is required' }); return; } - const deletedSite = await Site.findOneAndDelete({ name: siteData.name }); + const deletedSite = await Site.findOneAndDelete({ identity }); if (!deletedSite) { res.status(404).json({ error: 'Site not found' }); diff --git a/src/test/secure-site.test.ts b/src/test/secure-site.test.ts index 9f05fdb..46e8850 100644 --- a/src/test/secure-site.test.ts +++ b/src/test/secure-site.test.ts @@ -9,7 +9,7 @@ import { Site } from '../models/site'; describe('secure-site event handlers', function () { describe('PUT /api/secure-site', function () { - it('400 if name missing', async function () { + it('400 if identity missing', async function () { const req = httpMocks.createRequest({ method: 'PUT', body: {} }); const res = httpMocks.createResponse(); @@ -17,7 +17,7 @@ describe('secure-site event handlers', function () { assert.strictEqual(res._getStatusCode(), 400); assert.deepStrictEqual(res._getJSONData(), { - error: 'Site name is required', + error: 'Site identity is required', }); }); @@ -26,7 +26,7 @@ describe('secure-site event handlers', function () { const req = httpMocks.createRequest({ method: 'PUT', - body: { name: 'X' }, + body: { identity: 'test-identity' }, }); const res = httpMocks.createResponse(); @@ -41,7 +41,7 @@ describe('secure-site event handlers', function () { const req = httpMocks.createRequest({ method: 'PUT', - body: { name: 'X' }, + body: { identity: 'test-identity', name: 'X' }, }); const res = httpMocks.createResponse(); @@ -60,7 +60,7 @@ describe('secure-site event handlers', function () { const req = httpMocks.createRequest({ method: 'PUT', - body: { name: 'X' }, + body: { identity: 'test-identity', name: 'X' }, }); const res = httpMocks.createResponse(); @@ -86,9 +86,10 @@ describe('secure-site event handlers', function () { await postSecureSite(req, res); assert.strictEqual(res._getStatusCode(), 201); - assert.deepStrictEqual(res._getJSONData(), { - message: 'Site created successfully', - }); + // The actual identity value will be a SHA256 hash of the name + const response = res._getJSONData(); + assert.strictEqual(response.message, 'Site created successfully'); + assert.ok(response.identity && typeof response.identity === 'string'); }); it('400 on validation error', async function () { @@ -136,15 +137,18 @@ describe('secure-site event handlers', function () { }); describe('DELETE /api/secure-site', function () { - it('400 if name missing', async function () { - const req = httpMocks.createRequest({ method: 'DELETE', body: {} }); + it('400 if identity missing', async function () { + const req = httpMocks.createRequest({ + method: 'DELETE', + body: { identity: '' }, + }); const res = httpMocks.createResponse(); await deleteSecureSite(req, res); assert.strictEqual(res._getStatusCode(), 400); assert.deepStrictEqual(res._getJSONData(), { - error: 'Site name is required', + error: 'Site identity is required', }); }); @@ -153,7 +157,7 @@ describe('secure-site event handlers', function () { const req = httpMocks.createRequest({ method: 'DELETE', - body: { name: 'X' }, + body: { identity: 'test-identity' }, }); const res = httpMocks.createResponse(); @@ -164,11 +168,14 @@ describe('secure-site event handlers', function () { }); it('200 on success', async function () { - (Site.findOneAndDelete as any) = async () => ({ name: 'X' }); + (Site.findOneAndDelete as any) = async () => ({ + identity: 'test-identity', + name: 'X', + }); const req = httpMocks.createRequest({ method: 'DELETE', - body: { name: 'X' }, + body: { identity: 'test-identity' }, }); const res = httpMocks.createResponse(); @@ -177,7 +184,10 @@ describe('secure-site event handlers', function () { assert.strictEqual(res._getStatusCode(), 200); assert.deepStrictEqual(res._getJSONData(), { message: 'Site deleted successfully', - site: { name: 'X' }, + site: { + identity: 'test-identity', + name: 'X', + }, }); }); @@ -188,7 +198,7 @@ describe('secure-site event handlers', function () { const req = httpMocks.createRequest({ method: 'DELETE', - body: { name: 'X' }, + body: { identity: 'test-identity' }, }); const res = httpMocks.createResponse(); diff --git a/src/types/schema.d.ts b/src/types/schema.d.ts index 454747e..782aae1 100644 --- a/src/types/schema.d.ts +++ b/src/types/schema.d.ts @@ -618,7 +618,6 @@ export interface paths { * @description Parses CSV data and stores it as both signal and measurement records. * If a group is specified, any existing data with that group will be removed first. * The CSV should include columns for date, time, coordinate, cell_id, dbm, ping, download_speed, and upload_speed. - * */ post: { parameters: { @@ -698,7 +697,6 @@ export interface paths { * @description Returns two lists: * 1. Registered users sorted by issue date (newest first) * 2. Pending users whose issue date is within the expiry display limit, sorted by issue date (newest first) - * */ post: { parameters: { @@ -825,7 +823,6 @@ export interface paths { * Uses Passport LDAP strategy which binds to the LDAP server with the provided credentials. * On success, creates a session and redirects to /api/success. * On failure, redirects to /api/failure. - * */ post: { parameters: { @@ -937,7 +934,6 @@ export interface paths { * Update site configuration * @description Updates the sites configuration file with provided data. * Requires user to be authenticated - will redirect to login page if not authenticated. - * */ post: { parameters: { @@ -1016,7 +1012,6 @@ export interface paths { * @description Creates a new user with a cryptographically secure identity using EC keys. * Generates keypairs, creates signatures, and stores user information. * Requires authentication - will redirect to login page if not authenticated. - * */ post: { parameters: { @@ -1151,7 +1146,7 @@ export interface paths { }; requestBody: { content: { - 'application/json': components['schemas']['Site']; + 'application/json': components['schemas']['NewSiteRequest']; }; }; responses: { @@ -1195,7 +1190,7 @@ export interface paths { }; /** * Delete a site - * @description Removes an existing site from the system + * @description Removes an existing site from the system using its unique identity */ delete: { parameters: { @@ -1206,7 +1201,13 @@ export interface paths { }; requestBody: { content: { - 'application/json': components['schemas']['Site']; + 'application/json': { + /** + * @description The unique identifier of the site to delete + * @example 9a8b7c6d5e4f3g2h1i + */ + identity: string; + }; }; }; responses: { @@ -1449,6 +1450,11 @@ export interface components { package_loss: number; }; Site: { + /** + * @description Unique identifier of the user to update + * @example 9a8b7c6d5e4f3g2h1i + */ + identity: string; /** * @description Name of the site * @example Filipino Community Center @@ -1487,14 +1493,55 @@ export interface components { /** @description Optional geographical boundary coordinates defining the site perimeter as [latitude, longitude] pairs */ boundary?: [number, number][]; }; - /** @example { + NewSiteRequest: { + /** + * @description Name of the site + * @example Filipino Community Center + */ + name: string; + /** + * Format: double + * @description Geographic latitude coordinate + * @example 47.681932654395915 + */ + latitude: number; + /** + * Format: double + * @description Geographic longitude coordinate + * @example -122.31829217664796 + */ + longitude: number; + /** + * @description Current status of the site + * @example active + * @enum {string} + */ + status: NewSiteRequest; + /** + * @description Physical address of the site + * @example 5740 Martin Luther King Jr Way S, Seattle, WA 98118 + */ + address: string; + /** @description Array of cell identifiers associated with the site */ + cell_id: string[]; + /** + * @description Optional color identifier for the site in hex code + * @example #FF5733 + */ + color?: string; + /** @description Optional geographical boundary coordinates defining the site perimeter as [latitude, longitude] pairs */ + boundary?: [number, number][]; + }; + /** + * @example { * "Filipino Community Center": { * "ping": 115.28, * "download_speed": 7.16, * "upload_speed": 8.63, * "dbm": -78.4 * } - * } */ + * } + */ SitesSummary: { [key: string]: { /** @@ -1915,3 +1962,8 @@ export enum SiteStatus { confirmed = 'confirmed', in_conversation = 'in-conversation', } +export enum NewSiteRequestStatus { + active = 'active', + confirmed = 'confirmed', + in_conversation = 'in-conversation', +} From bd494c3afd7d55ea895430c2179727a12868e82b Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Mon, 27 Oct 2025 16:38:45 -0700 Subject: [PATCH 22/24] refactor: remove extra comments --- src/routes/secure-site.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/routes/secure-site.ts b/src/routes/secure-site.ts index 1f7c7e7..e8ccb00 100644 --- a/src/routes/secure-site.ts +++ b/src/routes/secure-site.ts @@ -37,13 +37,6 @@ export const putSecureSite = async (req: Request, res: Response) => { }; // Create a new site -// Summary of changes: -// Site has all the old fields, but there is a new field called identity -// Fill that field with -// import * as Crypto from 'crypto'; -// Crypto.createHash('sha256') -// .update(skpk) -// .digest('hex'); export const postSecureSite = async (req: Request, res: Response) => { try { const siteData: NewSiteRequest = req.body; @@ -73,7 +66,6 @@ export const postSecureSite = async (req: Request, res: Response) => { }; // Delete an existing site -// The request should just be the string with the identity (check schema.d.ts) export const deleteSecureSite = async (req: Request, res: Response) => { try { const { identity } = req.body; From 7b929af0909d29a355748dd7ae4798c251271a69 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Sun, 2 Nov 2025 22:46:58 -0800 Subject: [PATCH 23/24] refactor: change skpk to name on secure-site.ts --- src/routes/secure-site.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/secure-site.ts b/src/routes/secure-site.ts index e8ccb00..4ace4b2 100644 --- a/src/routes/secure-site.ts +++ b/src/routes/secure-site.ts @@ -42,8 +42,8 @@ export const postSecureSite = async (req: Request, res: Response) => { const siteData: NewSiteRequest = req.body; // Generate identity using SHA256 hash - const skpk = siteData.name; - const identity = crypto.createHash('sha256').update(skpk).digest('hex'); + const name = siteData.name; + const identity = crypto.createHash('sha256').update(name).digest('hex'); const siteWithIdentity: EditSiteRequest = { identity, From ec66969783b30c52d5b51ccdf610db4353461b16 Mon Sep 17 00:00:00 2001 From: Ananya Aatreya Date: Mon, 10 Nov 2025 21:23:13 -0800 Subject: [PATCH 24/24] feat: replace Hash with UUID --- src/routes/secure-site.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/routes/secure-site.ts b/src/routes/secure-site.ts index 4ace4b2..7091fcf 100644 --- a/src/routes/secure-site.ts +++ b/src/routes/secure-site.ts @@ -40,10 +40,7 @@ export const putSecureSite = async (req: Request, res: Response) => { export const postSecureSite = async (req: Request, res: Response) => { try { const siteData: NewSiteRequest = req.body; - - // Generate identity using SHA256 hash - const name = siteData.name; - const identity = crypto.createHash('sha256').update(name).digest('hex'); + const identity = crypto.randomUUID(); const siteWithIdentity: EditSiteRequest = { identity,