diff --git a/deploy.sh b/deploy.sh index daa6b63..e03b995 100755 --- a/deploy.sh +++ b/deploy.sh @@ -31,7 +31,7 @@ SSH_OPTS="-i $DEPLOY_SSH_KEY" echo "→ Deploying to $DEPLOY_USER@$DEPLOY_HOST using key $DEPLOY_SSH_KEY" -# 1. Build frontend (uses indexLocal.html — no Crisp; see vite --mode nocrisp) +# 1. Build frontend for LAN / self-contained hosts (indexLocal.html via vite --mode localnet) ( cd "$SCRIPT_DIR" npm run build:local diff --git a/indexLocal.html b/indexLocal.html index d31ab53..af3867c 100644 --- a/indexLocal.html +++ b/indexLocal.html @@ -1,4 +1,5 @@ - + diff --git a/package.json b/package.json index 6dba33e..8531516 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "start": "VITE_AUTH0_CLIENT_ID=5pUCkl2SfogkWmM244UDcLEUOp8EFdHd VITE_AUTH0_AUDIENCE=api.brandxtech.ca VITE_APP_API_URL=https://api.brandxtech.ca/v1 VITE_APP_WS_URL=ws://api.brandxtech.ca/v1/live vite", - "start-local": "VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_APP_API_URL=http://localhost:50052/v1 VITE_APP_WS_URL=ws://localhost:50052/v1/live VITE_APP_BILLING_URL=http://localhost:50053/v1 VITE_APP_RECLUSE_URL=http://localhost:50054/v1 VITE_APP_GITLAB_URL=http://localhost:50055/v1 vite --mode nocrisp", + "start-local": "VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_APP_API_URL=http://localhost:50052/v1 VITE_APP_WS_URL=ws://localhost:50052/v1/live VITE_APP_BILLING_URL=http://localhost:50053/v1 VITE_APP_RECLUSE_URL=http://localhost:50054/v1 VITE_APP_GITLAB_URL=http://localhost:50055/v1 vite --mode localnet", "start-dev": "VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_APP_API_URL=https://bxt-dev.api.adaptiveagriculture.ca/v1 VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_AUTH0_DEV_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 vite", "start-streamline": "VITE_AUTH0_CLIENT_ID=HwUV0hHNdVvU96zuMBTAU8i7nFdwwgIX VITE_APP_API_URL=https://streamline.api.adaptiveagriculture.ca/v1 VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=streamline.api.adaptiveagriculture.ca vite", "start-staging": "VITE_LOCAL_STAGING=true VITE_AUTH0_CLIENT_ID=3ib460VvLwdeyse5iUSQfxkVdQaUmphP VITE_AUTH0_AUDIENCE=stagingapi.brandxtech.ca VITE_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com VITE_APP_API_URL=https://stagingapi.brandxtech.ca/v1 VITE_APP_WS_URL=ws://stagingapi.brandxtech.ca/v1/live VITE_APP_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com VITE_APP_AUTH0_AUDIENCE=stagingapi.brandxtech.ca vite", @@ -16,7 +16,7 @@ "build:development": "VITE_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_APP_API_URL=https://bxt-dev.api.adaptiveagriculture.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_DEVELOPMENT} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP=${OPEN_WEATHERMAP} vite build", "build:production": "VITE_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} VITE_AUTH0_CLIENT_ID=5pUCkl2SfogkWmM244UDcLEUOp8EFdHd VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=api.brandxtech.ca VITE_APP_API_URL=https://api.brandxtech.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_PRODUCTION} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP=${OPEN_WEATHERMAP} vite build", "build:streamline": "VITE_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} VITE_AUTH0_CLIENT_ID=HwUV0hHNdVvU96zuMBTAU8i7nFdwwgIX VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=streamline.api.adaptiveagriculture.ca VITE_APP_API_URL=https://streamline.api.adaptiveagriculture.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_PRODUCTION} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP=${OPEN_WEATHERMAP} vite build", - "build:local": "VITE_AUTH0_CLIENT_ID=local VITE_AUTH0_AUDIENCE=local VITE_AUTH0_CLIENT_DOMAIN=local VITE_APP_API_URL=http://172.16.1.20:50052/v1 VITE_APP_WS_URL=ws://172.16.1.20:50052/v1/live NODE_OPTIONS=--max_old_space_size=4096 vite build --mode nocrisp", + "build:local": "VITE_AUTH0_CLIENT_ID=local VITE_AUTH0_AUDIENCE=local VITE_AUTH0_CLIENT_DOMAIN=local VITE_APP_API_URL=http://172.16.1.20:50052/v1 VITE_APP_WS_URL=ws://172.16.1.20:50052/v1/live NODE_OPTIONS=--max_old_space_size=4096 vite build --mode localnet", "build:offline": "npx env-cmd offline,whitelabel npm run build", "test": "vitest" }, diff --git a/src/app/LocalAuthPlaceholder.tsx b/src/app/LocalAuthPlaceholder.tsx new file mode 100644 index 0000000..cc05526 --- /dev/null +++ b/src/app/LocalAuthPlaceholder.tsx @@ -0,0 +1,61 @@ +import { Box, Button, CssBaseline, Paper, Stack, Typography } from '@mui/material' +import { getName } from 'services/whiteLabel' + +export type LocalAuthPlaceholderReason = 'unconfigured' | 'insecure_origin' + +interface Props { + /** Why cloud Auth0 was skipped — copy differs when env is set but the page is not a secure context (e.g. http://LAN IP). */ + reason?: LocalAuthPlaceholderReason +} + +/** + * Shown when Auth0 is not used (missing config and/or non–secure context). + * Replace with backend-driven login once local auth is implemented. + */ +export default function LocalAuthPlaceholder(props: Props) { + const { reason = 'unconfigured' } = props + const productName = getName() + + const explanation = + reason === 'insecure_origin' + ? 'This URL is not a secure context for cloud sign-in (Auth0 requires HTTPS, localhost, or 127.0.0.1). Use local account sign-in here instead once it is connected.' + : 'Cloud sign-in (Auth0) is not configured for this deployment. Local account sign-in will be available here.' + + return ( + <> + + + + + + {productName} + + + {explanation} + + + + + + + Buttons are placeholders until the backend login flow is wired up. + + + + + + ) +} diff --git a/src/utils/auth0Config.ts b/src/utils/auth0Config.ts new file mode 100644 index 0000000..1c5e568 --- /dev/null +++ b/src/utils/auth0Config.ts @@ -0,0 +1,24 @@ +import { getWhitelabel } from 'services/whiteLabel' + +/** True when Auth0 env + whitelabel client ID are present; avoids mounting Auth0Provider on offline / local LAN builds. */ +export function isAuth0Configured(): boolean { + const wl = getWhitelabel() + const domain = String(import.meta.env.VITE_AUTH0_CLIENT_DOMAIN ?? '').trim() + const clientRaw = wl.auth0ClientId ?? import.meta.env.VITE_AUTH0_CLIENT_ID + const clientId = String(clientRaw ?? '').trim() + return domain.length > 0 && clientId.length > 0 +} + +/** + * auth0-spa-js only runs in a "secure context" (HTTPS, http://localhost, http://127.0.0.1, etc.). + * Plain http://192.168.x.x fails — same check as `window.isSecureContext`. + */ +export function isAuth0SpaOriginAllowed(): boolean { + if (typeof window === 'undefined') return true + return window.isSecureContext +} + +/** Mount Auth0 only when credentials exist and the browser will let the SDK run. */ +export function shouldMountAuth0Provider(): boolean { + return isAuth0Configured() && isAuth0SpaOriginAllowed() +} diff --git a/vite.config.ts b/vite.config.ts index bc0fc1b..ed1b6f1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,14 +8,15 @@ import { fileURLToPath } from 'node:url' const rootDir = path.dirname(fileURLToPath(import.meta.url)) -const NO_CRISP_MODE = 'nocrisp' +const LOCALNET_MODE = 'localnet' -function useNoCrispIndexHtml (mode: string, command: string): Plugin { - if (mode !== NO_CRISP_MODE || command !== 'serve') { - return { name: 'use-local-index-html-noop' } +/** Dev server: serve the minimal LAN shell (indexLocal.html) instead of the full index with third-party bootstraps. */ +function useLocalnetShellHtml (mode: string, command: string): Plugin { + if (mode !== LOCALNET_MODE || command !== 'serve') { + return { name: 'localnet-shell-html-noop' } } return { - name: 'use-local-index-html', + name: 'localnet-shell-html', apply: 'serve', transformIndexHtml: { order: 'pre', @@ -26,12 +27,13 @@ function useNoCrispIndexHtml (mode: string, command: string): Plugin { } } -function emitNoCrispIndexAsIndexHtml (mode: string): Plugin { - if (mode !== NO_CRISP_MODE) { - return { name: 'emit-nocrisp-index-as-index-noop' } +/** After build, LAN shell is emitted as indexLocal.html; rename to index.html so nginx and PWA still use /. */ +function emitLocalnetShellAsIndexHtml (mode: string): Plugin { + if (mode !== LOCALNET_MODE) { + return { name: 'emit-localnet-shell-as-index-noop' } } return { - name: 'emit-nocrisp-index-as-index-html', + name: 'emit-localnet-shell-as-index-html', closeBundle () { const outDir = path.join(rootDir, 'build') const from = path.join(outDir, 'indexLocal.html') @@ -46,11 +48,11 @@ function emitNoCrispIndexAsIndexHtml (mode: string): Plugin { // https://vitejs.dev/config/ export default defineConfig(({ command, mode }): UserConfig => { - const useNoCrispIndex = mode === NO_CRISP_MODE + const useLocalnetShell = mode === LOCALNET_MODE return { plugins: [ - useNoCrispIndexHtml(mode, command), - emitNoCrispIndexAsIndexHtml(mode), + useLocalnetShellHtml(mode, command), + emitLocalnetShellAsIndexHtml(mode), react(), tsconfigPaths(), VitePWA({ @@ -99,7 +101,7 @@ export default defineConfig(({ command, mode }): UserConfig => { input: { main: path.join( rootDir, - useNoCrispIndex ? 'indexLocal.html' : 'index.html' + useLocalnetShell ? 'indexLocal.html' : 'index.html' ), }, }