From cfa50ea09bf434798d914c93b4c626ca3fddb51e Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Thu, 29 Jun 2023 14:52:18 +0200
Subject: [PATCH] feat: allow sapi endpoint to be configured at ... ...runtime

---
 common/constants.js                      |  3 +
 deploy/app.js                            | 73 ++++++++++--------------
 docs/releases/v2.12.0.md                 |  3 +-
 src/atlasComponents/sapi/sapi.service.ts |  8 ++-
 src/atlasViewer/atlasViewer.component.ts |  4 +-
 5 files changed, 44 insertions(+), 47 deletions(-)

diff --git a/common/constants.js b/common/constants.js
index d3fe2b2b3..d3f6f2454 100644
--- a/common/constants.js
+++ b/common/constants.js
@@ -148,6 +148,9 @@ If you do not accept the Terms & Conditions you are not permitted to access or u
     REMOVE_FRONTAL_OCTANT_HELPER_TEXT: `Hide the octant facing the user, and overlaying the slice views.`,
 
     AUXMESH_DESC: `Some templates contain auxiliary meshes, which compliment the appearance of the template in the perspective view.`,
+
+    OVERWRITE_SAPI_ENDPOINT_ATTR: `x-sapi-base-url`,
+    DATA_ERROR_ATTR: `data-error`
   }
 
   exports.QUICKTOUR_DESC ={
diff --git a/deploy/app.js b/deploy/app.js
index f4b3dcd07..bc3444df3 100644
--- a/deploy/app.js
+++ b/deploy/app.js
@@ -1,4 +1,3 @@
-const fs = require('fs')
 const path = require('path')
 const express = require('express')
 const app = express.Router()
@@ -6,8 +5,7 @@ const session = require('express-session')
 const crypto = require('crypto')
 const cookieParser = require('cookie-parser')
 const bkwdMdl = require('./bkwdCompat')()
-
-const LOCAL_CDN_FLAG = !!process.env.LOCAL_CDN
+const { CONST } = require("../common/constants")
 
 if (process.env.NODE_ENV !== 'production') {
   app.use(require('cors')())
@@ -123,36 +121,6 @@ const PUBLIC_PATH = process.env.NODE_ENV === 'production'
  */
 app.use('/.well-known', express.static(path.join(__dirname, 'well-known')))
 
-if (LOCAL_CDN_FLAG) {
-  /*
-  * TODO setup local cdn for supported libraries map
-  */
-  const LOCAL_CDN = process.env.LOCAL_CDN
-  const CDN_ARRAY = [
-    'https://stackpath.bootstrapcdn.com',
-    'https://use.fontawesome.com',
-    'https://unpkg.com'
-  ]
-
-  let indexFile
-  fs.readFile(path.join(PUBLIC_PATH, 'index.html'), 'utf-8', (err, data) => {
-    if (err) throw err
-    if (!LOCAL_CDN) {
-      indexFile = data
-      return
-    }
-    const regexString = CDN_ARRAY.join('|').replace(/\/|\./g, s => `\\${s}`)
-    const regex = new RegExp(regexString, 'gm')
-    indexFile = data.replace(regex, LOCAL_CDN)
-  })
-  
-  app.get('/', bkwdMdl, (_req, res) => {
-    if (!indexFile) return res.status(404).end()
-    res.setHeader('Content-Type', 'text/html; charset=utf-8')
-    return res.status(200).send(indexFile)
-  })
-}
-
 app.use((_req, res, next) => {
   res.setHeader('Referrer-Policy', 'origin-when-cross-origin')
   next()
@@ -182,23 +150,42 @@ app.get('/', (req, res, next) => {
     middelware(req, res, next)
   }
 
-}, bkwdMdl, cookieParser(), (req, res) => {
+}, bkwdMdl, cookieParser(), async (req, res) => {
+  res.setHeader('Content-Type', 'text/html')
+
+  let returnIndex = indexTemplate
+
+  if (!!process.env.LOCAL_CDN) {
+    const CDN_ARRAY = [
+      'https://stackpath.bootstrapcdn.com',
+      'https://use.fontawesome.com',
+      'https://unpkg.com'
+    ]
+  
+    const regexString = CDN_ARRAY.join('|').replace(/\/|\./g, s => `\\${s}`)
+    const regex = new RegExp(regexString, 'gm')
+    returnIndex = returnIndex.replace(regex, process.env.LOCAL_CDN)
+  }
   const iavError = req.cookies && req.cookies['iav-error']
   
-  res.setHeader('Content-Type', 'text/html')
+  const attributeToAppend = {}
 
   if (iavError) {
     res.clearCookie('iav-error', { httpOnly: true, sameSite: 'strict' })
+    attributeToAppend[CONST.DATA_ERROR_ATTR] = iavError
+  }
 
-    const returnTemplate = indexTemplate
-      .replace(/\$\$NONCE\$\$/g, res.locals.nonce)
-      .replace('<atlas-viewer>', `<atlas-viewer data-error="${iavError.replace(/"/g, '&quot;')}">`)
-    res.status(200).send(returnTemplate)
-  } else {
-    const returnTemplate = indexTemplate
-      .replace(/\$\$NONCE\$\$/g, res.locals.nonce)
-    res.status(200).send(returnTemplate)
+  if (!!process.env.OVERWRITE_API_ENDPOING) {
+    attributeToAppend[CONST.OVERWRITE_SAPI_ENDPOINT_ATTR] = process.env.OVERWRITE_API_ENDPOING
   }
+  
+  const attr = Object.entries(attributeToAppend).map(([key, value]) => `${key}="${value.replace(/"/g, '&quot;')}"`).join(" ")
+
+  const returnTemplate = returnIndex
+    .replace(/\$\$NONCE\$\$/g, res.locals.nonce)
+    .replace('<atlas-viewer>', `<atlas-viewer ${attr}>`)
+
+  res.status(200).send(returnTemplate)
 })
 
 app.get('/ready', async (req, res) => {
diff --git a/docs/releases/v2.12.0.md b/docs/releases/v2.12.0.md
index ba23e2fa1..99ad15e06 100644
--- a/docs/releases/v2.12.0.md
+++ b/docs/releases/v2.12.0.md
@@ -13,4 +13,5 @@
 ## Behind the scene
 
 - update spotlight mechanics from in-house to angular CDK
-- Updated neuroglancer/nehuba dependency. This allows volumes with non-rigid affine to be displayed properly.
+- updated neuroglancer/nehuba dependency. This allows volumes with non-rigid affine to be displayed properly.
+- allow siibra-api endpoint to be configured at runtime
diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts
index b5aaf5bc4..0fec93688 100644
--- a/src/atlasComponents/sapi/sapi.service.ts
+++ b/src/atlasComponents/sapi/sapi.service.ts
@@ -13,6 +13,7 @@ import {
 import { FeatureType, PathReturn, RouteParam, SapiRoute } from "./typeV3";
 import { BoundingBox, SxplrAtlas, SxplrParcellation, SxplrRegion, SxplrTemplate, VoiFeature, Feature } from "./sxplrTypes";
 import { parcBanList, speciesOrder } from "src/util/constants";
+import { CONST } from "common/constants"
 
 export const useViewer = {
   THREESURFER: "THREESURFER",
@@ -94,7 +95,12 @@ export class SAPI{
    */
   static get BsEndpoint$(): Observable<string> {
     if (!!BS_ENDPOINT_CACHED_VALUE) return BS_ENDPOINT_CACHED_VALUE
-    const endpoints = environment.SIIBRA_API_ENDPOINTS.split(',')
+    const rootEl = document.querySelector('atlas-viewer')
+    const overwriteSapiUrl = rootEl.getAttribute(CONST.OVERWRITE_SAPI_ENDPOINT_ATTR)
+    
+    const endpoints = overwriteSapiUrl
+      ? [ overwriteSapiUrl ]
+      : environment.SIIBRA_API_ENDPOINTS.split(',')
     if (endpoints.length === 0) {
       SAPI.ErrorMessage = `No siibra-api endpoint defined!`
       return NEVER
diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts
index c28d15f9a..b80a7c4de 100644
--- a/src/atlasViewer/atlasViewer.component.ts
+++ b/src/atlasViewer/atlasViewer.component.ts
@@ -76,11 +76,11 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit {
     @Inject(DARKTHEME) private darktheme$: Observable<boolean>
   ) {
 
-    const error = this.el.nativeElement.getAttribute('data-error')
+    const error = this.el.nativeElement.getAttribute(CONST.DATA_ERROR_ATTR)
 
     if (error) {
       this.snackbar.open(error, 'Dismiss', { duration: 5000 })
-      this.el.nativeElement.removeAttribute('data-error')
+      this.el.nativeElement.removeAttribute(CONST.DATA_ERROR_ATTR)
     }
   }
 
-- 
GitLab