From 621a885f02bc3a547a87410d387fe144f8c3fdb6 Mon Sep 17 00:00:00 2001 From: Mentor Date: Tue, 17 Oct 2023 17:55:03 +0200 Subject: [PATCH] Restructure backend --- firestore.indexes.json | 20 +++++++- functions/index.js | 60 +++++++++++------------- functions/modules/health.js | 23 ++++++--- functions/runtime/on_call_runtimes.js | 52 ++++++++++++++++++++ functions/runtime/on_request_runtimes.js | 37 +++++++++++++++ functions/runtime/runtimes_settings.js | 27 +++++++++++ 6 files changed, 178 insertions(+), 41 deletions(-) create mode 100644 functions/runtime/on_call_runtimes.js create mode 100644 functions/runtime/on_request_runtimes.js create mode 100644 functions/runtime/runtimes_settings.js diff --git a/firestore.indexes.json b/firestore.indexes.json index 7aa4d60..49312dd 100644 --- a/firestore.indexes.json +++ b/firestore.indexes.json @@ -71,7 +71,25 @@ "order": "ASCENDING" } ] + }, + { + "collectionGroup": "static_drop_claims", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "is_mock_claim", + "order": "ASCENDING" + }, + { + "fieldPath": "expires", + "order": "ASCENDING" + }, + { + "fieldPath": "__name__", + "order": "ASCENDING" + } + ] } ], "fieldOverrides": [] -} +} \ No newline at end of file diff --git a/functions/index.js b/functions/index.js index 1a0a7e1..ee0e315 100644 --- a/functions/index.js +++ b/functions/index.js @@ -1,25 +1,19 @@ // V1 Dependencies const functions = require( "firebase-functions" ) -// V2 Dependencies -const { onRequest, onCall } = require( "firebase-functions/v2/https" ) - -// V2 Runtime config -const protected_runtime = { - enforceAppCheck: true, -} - // V1 Runtime config const generousRuntime = { timeoutSeconds: 540, memory: '4GB' } -const keepWarmRuntime = { - minInstances: 1, -} + const { log, dev } = require( './modules/helpers' ) log( `⚠️ Verbose mode on, ${ dev ? '⚙️ dev mode on' : '🚀 production mode on' }` ) +// Runtime config +const { v1_oncall, v2_oncall } = require( './runtime/on_call_runtimes' ) +const { v1_onrequest, v2_onrequest } = require( './runtime/on_request_runtimes' ) + // /////////////////////////////// // Code status managers // /////////////////////////////// @@ -27,47 +21,47 @@ log( `⚠️ Verbose mode on, ${ dev ? '⚙️ dev mode on' : '🚀 production m const { refreshScannedCodesStatuses, refresh_unknown_and_unscanned_codes, getEventDataFromCode, check_code_status } = require( './modules/codes' ) // Get event data of a code -exports.getEventDataFromCode = functions.https.onCall( getEventDataFromCode ) +exports.getEventDataFromCode = v1_oncall( getEventDataFromCode ) // Get all data of a code -exports.check_code_status = functions.https.onCall( check_code_status ) +exports.check_code_status = v1_oncall( check_code_status ) // Refresh all codes ( trigger from frontend on page mount of EventView ) -exports.requestManualCodeRefresh = functions.runWith( generousRuntime ).https.onCall( refresh_unknown_and_unscanned_codes ) +exports.requestManualCodeRefresh = v2_oncall( [ 'high_memory', 'long_timeout' ], refresh_unknown_and_unscanned_codes ) // Allow frontend to trigger updates for scanned codes, ( triggers on a periodic interval from EventView ), is lighter than requestManualCodeRefresh as it checks only scanned and claimed == true codes -exports.refreshScannedCodesStatuses = functions.runWith( generousRuntime ).https.onCall( refreshScannedCodesStatuses ) +exports.refreshScannedCodesStatuses = v1_oncall( [ 'high_memory', 'long_timeout' ], refreshScannedCodesStatuses ) // Directly mint a code to an address const { mint_code_to_address } = require( './modules/minting' ) -exports.mint_code_to_address = onCall( protected_runtime, mint_code_to_address ) +exports.mint_code_to_address = v2_oncall( [ 'high_memory', 'long_timeout' ], mint_code_to_address ) // Let admins recalculate available codes const { recalculate_available_codes_admin } = require( './modules/codes' ) -exports.recalculate_available_codes = onCall( protected_runtime, recalculate_available_codes_admin ) +exports.recalculate_available_codes = v2_oncall( recalculate_available_codes_admin ) // /////////////////////////////// // Event data // /////////////////////////////// const { registerEvent, deleteEvent, getUniqueOrganiserEmails } = require( './modules/events' ) -exports.registerEvent = functions.runWith( generousRuntime ).https.onCall( registerEvent ) -exports.deleteEvent = functions.https.onCall( deleteEvent ) +exports.registerEvent = v1_oncall( [ 'high_memory', 'long_timeout' ], registerEvent ) +exports.deleteEvent = v1_oncall( deleteEvent ) // Email export to update event organisers -exports.getUniqueOrganiserEmails = functions.https.onCall( getUniqueOrganiserEmails ) +exports.getUniqueOrganiserEmails = v1_oncall( getUniqueOrganiserEmails ) // /////////////////////////////// // QR Middleware API // /////////////////////////////// const claimMiddleware = require( './modules/claim' ) -exports.claimMiddleware = onRequest( { cors: true, ...keepWarmRuntime }, claimMiddleware ) +exports.claimMiddleware = v2_onrequest( [ 'max_concurrency', 'keep_warm', 'memory' ], claimMiddleware ) /* /////////////////////////////// // Kiosk generator middleware API // /////////////////////////////*/ const generate_kiosk = require( './modules/kiosk_generator' ) -exports.generate_kiosk = functions.https.onRequest( generate_kiosk ) +exports.generate_kiosk = v1_onrequest( generate_kiosk ) // /////////////////////////////// // Housekeeping @@ -90,36 +84,36 @@ exports.updateEventAvailableCodes = functions.firestore.document( `codes/{codeId // Security // /////////////////////////////*/ const { validateCallerDevice, validateCallerCaptcha } = require( './modules/security' ) -exports.validateCallerDevice = onCall( { ...protected_runtime, ...keepWarmRuntime, }, validateCallerDevice ) -exports.validateCallerCaptcha = functions.https.onCall( validateCallerCaptcha ) +exports.validateCallerDevice = v2_oncall( [ 'high_memory', 'long_timeout', 'keep_warm' ], validateCallerDevice ) +exports.validateCallerCaptcha = v1_oncall( validateCallerCaptcha ) // Log kiosk opens const { log_kiosk_open } = require( './modules/security' ) -exports.log_kiosk_open = onCall( protected_runtime, log_kiosk_open ) +exports.log_kiosk_open = v2_oncall( log_kiosk_open ) /* /////////////////////////////// // Code claiming // /////////////////////////////*/ const { get_code_by_challenge } = require( './modules/codes' ) -exports.get_code_by_challenge = functions.https.onCall( get_code_by_challenge ) +exports.get_code_by_challenge = v1_oncall( get_code_by_challenge ) /* /////////////////////////////// // Health check // /////////////////////////////*/ const { health_check, public_health_check } = require( './modules/health' ) -exports.health_check = functions.https.onCall( health_check ) -exports.ping = functions.https.onCall( ping => 'pong' ) +exports.health_check = v1_oncall( health_check ) +exports.ping = v2_oncall( [ 'max_concurrency' ],ping => 'pong' ) /* /////////////////////////////// // Static QR system // /////////////////////////////*/ const { claim_code_by_email } = require( './modules/codes' ) const { export_emails_of_static_drop, create_static_drop, update_public_static_drop_data, delete_emails_of_static_drop } = require( './modules/static_qr_drop' ) -exports.export_emails_of_static_drop = functions.https.onCall( export_emails_of_static_drop ) -exports.delete_emails_of_static_drop = functions.https.onCall( delete_emails_of_static_drop ) -exports.claim_code_by_email = functions.https.onCall( claim_code_by_email ) -exports.create_static_drop = functions.https.onCall( create_static_drop ) +exports.export_emails_of_static_drop = v1_oncall( export_emails_of_static_drop ) +exports.delete_emails_of_static_drop = v1_oncall( delete_emails_of_static_drop ) +exports.claim_code_by_email = v1_oncall( claim_code_by_email ) +exports.create_static_drop = v1_oncall( create_static_drop ) exports.update_public_static_drop_data = functions.firestore.document( `static_drop_private/{drop_id}` ).onWrite( update_public_static_drop_data ) // Public health check -exports.public_health_check = functions.https.onRequest( public_health_check ) \ No newline at end of file +exports.public_health_check = v1_onrequest( public_health_check ) \ No newline at end of file diff --git a/functions/modules/health.js b/functions/modules/health.js index 5830cee..40f999d 100644 --- a/functions/modules/health.js +++ b/functions/modules/health.js @@ -1,6 +1,9 @@ const { log } = require( './helpers' ) const { db } = require( './firebase' ) +let cached_api_health = false +let cached_api_health_timestamp = 0 +const cached_api_health_duration_ms = 10_000 const health_check = async () => { // Function dependencies @@ -20,15 +23,21 @@ const health_check = async () => { return false } ) - // Check the self-reported health of the POAP api - const api_health = await call_poap_endpoint( `/health-check` ).catch( e => { - log( e ) - return false - } ) + // if the cached api health is stale, check the api health + const api_health_cache_stale = Date.now() - cached_api_health_timestamp > cached_api_health_duration_ms + if( api_health_cache_stale ) { + + // Check the self-reported health of the POAP api + cached_api_health = await call_poap_endpoint( `/health-check` ).catch( e => { + log( e ) + return false + } ) + } + // Update status object to reflect new data - status.healthy = !!( has_token && api_health ) - status.poap_api = !!api_health + status.healthy = !!( has_token && cached_api_health ) + status.poap_api = !!cached_api_health status.poap_api_auth = !!has_token return status diff --git a/functions/runtime/on_call_runtimes.js b/functions/runtime/on_call_runtimes.js new file mode 100644 index 0000000..cae0a56 --- /dev/null +++ b/functions/runtime/on_call_runtimes.js @@ -0,0 +1,52 @@ +// Impport dev and log helpers +const { log } = require( '../modules/helpers' ) +const debug = false + +/** + * Return a V1 oncall with runtimes. NOTE: v1 appcheck is enforced through code and not config + * @param {Array.<"high_memory"|"long_timeout"|"keep_warm">} [runtimes] - Array of runtime keys to use + * @param {Function} handler - Function to run + * @returns {Function} - Firebase function +*/ +exports.v1_oncall = ( runtimes=[], handler ) => { + + if( debug ) log( `Creating handler with: `, typeof runtimes, runtimes, typeof handler, handler ) + + const functions = require( "firebase-functions" ) + const { v1_runtimes } = require( './runtimes_settings' ) + + // If the first parameter was a function, return the undecorated handler + if( typeof runtimes === 'function' ) { + if( debug ) log( 'v1_oncall: no runtimes specified, returning undecorated handler' ) + return functions.https.onCall( runtimes ) + } + + // Config the runtimes for this function + const runtime = runtimes.reduce( ( acc, runtime_key ) => ( { ...acc, ...v1_runtimes[ runtime_key ] } ), {} ) + if( debug ) log( 'v1_oncall: returning decorated handler with runtime: ', runtime ) + return functions.runWith( runtime ).https.onCall( handler ) +} + +/** + * Return a V2 oncall with runtimes + * @param {Array.<"long_timeout"|"high_memory"|"keep_warm"|"max_concurrency">} [runtimes] - Array of runtime keys to use, the protected runtime is ALWAYS ADDED + * @param {Function} handler - Firebase function handler + * @returns {Function} - Firebase function + */ +exports.v2_oncall = ( runtimes=[], handler ) => { + + if( debug ) log( `Creating handler with: `, typeof runtimes, runtimes, typeof handler, handler ) + + const { onCall } = require( "firebase-functions/v2/https" ) + const { v2_runtimes } = require( './runtimes_settings' ) + + // If the first parameter was a function, return the handler as 'protected' firebase oncall + if( typeof runtimes === 'function' ) { + if( debug ) log( 'v2_oncall: no runtimes specified, returning undecorated handler' ) + return onCall( { ...v2_runtimes.protected }, runtimes ) + } + + const runtime = runtimes.reduce( ( acc, runtime_key ) => ( { ...acc, ...v2_runtimes[ runtime_key ] } ), { ...v2_runtimes.protected } ) + if( debug ) log( 'v2_oncall: returning decorated handler with runtime: ', runtime ) + return onCall( runtime, handler ) +} \ No newline at end of file diff --git a/functions/runtime/on_request_runtimes.js b/functions/runtime/on_request_runtimes.js new file mode 100644 index 0000000..12b0c6c --- /dev/null +++ b/functions/runtime/on_request_runtimes.js @@ -0,0 +1,37 @@ +/** + * Return a V1 onRequest with runtimes. NOTE: v1 appcheck is enforced through code and not config + * @param {Array.<"high_memory"|"long_timeout"|"keep_warm">} [runtimes] - Array of runtime keys to use + * @param {Function} handler - Function to run + * @returns {Function} - Firebase function +*/ +exports.v1_onrequest = ( runtimes=[], handler ) => { + + const functions = require( "firebase-functions" ) + const { v1_runtimes } = require( './runtimes_settings' ) + + // If the first parameter was a function, return the undecorated handler + if( typeof runtimes === 'function' ) return functions.https.onRequest( runtimes ) + + // Config the runtimes for this function + const runtime = runtimes.reduce( ( acc, runtime_key ) => ( { ...acc, ...v1_runtimes[ runtime_key ] } ), {} ) + return functions.runWith( runtime ).https.onRequest( handler ) +} + +/** + * Return a V2 onRequest with runtimes + * @param {Array.<"long_timeout"|"high_memory"|"keep_warm"|"max_concurrency">} [runtimes] - Array of runtime keys to use, CORS support is ALWAYS ADDED + * @param {Function} handler - Firebase function handler + * @returns {Function} - Firebase function + */ +exports.v2_onrequest = ( runtimes=[], handler ) => { + + const { onRequest } = require( "firebase-functions/v2/https" ) + const { v2_runtimes } = require( './runtimes_settings' ) + const runtime_basis = { cors: true } + + // If the first parameter was a function, return the handler as 'protected' firebase oncall + if( typeof runtimes === 'function' ) return onRequest( runtime_basis, runtimes ) + + const runtime = runtimes.reduce( ( acc, runtime_key ) => ( { ...acc, ...v2_runtimes[ runtime_key ] } ), runtime_basis ) + return onRequest( runtime, handler ) +} \ No newline at end of file diff --git a/functions/runtime/runtimes_settings.js b/functions/runtime/runtimes_settings.js new file mode 100644 index 0000000..6e25dee --- /dev/null +++ b/functions/runtime/runtimes_settings.js @@ -0,0 +1,27 @@ +/** + * @typedef {Object} V1runtimes + * @property {string} high_memory - Allocate high memory to function + * @property {string} long_timeout - Set long timeout to function + * @property {string} keep_warm - Keep function warm + */ +exports.v1_runtimes = { + high_memory: { memory: '4GB' }, + long_timeout: { timeoutSeconds: 540 }, + keep_warm: { minInstances: 1 }, +} + +/** + * @typedef {Object} V2runtimes + * @property {string} protected - Enforce appcheck + * @property {string} long_timeout - Set long timeout to function + * @property {string} high_memory - Allocate high memory to function + * @property {string} keep_warm - Keep function warm with min instances + * @property {string} max_concurrency - Set max concurrency + */ +exports.v2_runtimes = { + protected: { enforceAppCheck: true }, + long_timeout: { timeoutSeconds: 540 }, + high_memory: { memory: '4GiB' }, // Note: memory also increases compute power + keep_warm: { minInstances: 1 }, + max_concurrency: { concurrency: 1000 } +} \ No newline at end of file