From 0f23e847374e5590ca1ea260c9f6a3c0bc82b7f4 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Mon, 6 Apr 2026 17:33:53 +0530 Subject: [PATCH] feat: embed website widget on login page via admin config endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fetches /api/config/widget from the sidecar and injects widget.js only when enabled + embed.loginPage + key are all set. Falls back to VITE_API_URL as the script host when cfg.url is empty (default for fresh configs). Replaces an earlier draft that read VITE_WIDGET_KEY + VITE_WIDGET_URL from build-time env — widget config lives in data/widget.json on the sidecar now and is admin-editable via PUT /api/config/widget, so no rebuild is needed to toggle or rotate it. Never blocks login on a widget-config failure — the fetch is fire-and-forget and errors just log a warning. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pages/login.tsx | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/pages/login.tsx b/src/pages/login.tsx index 29410a7..bdece77 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faEye, faEyeSlash } from '@fortawesome/pro-duotone-svg-icons'; @@ -19,6 +19,46 @@ export const LoginPage = () => { const { isOpen, activeAction, close } = useMaintShortcuts(); const { tokens } = useThemeTokens(); + // Load website widget on login page. + // + // Config comes from the admin-editable sidecar endpoint (not env vars) + // so it can be toggled / rotated / moved at runtime. The widget renders + // only when all of enabled + embed.loginPage + key are set. + // + // `url` may be empty (default for fresh configs) — in that case we fall + // back to the same origin the app is talking to for its API, since in + // practice the sidecar serves /widget.js alongside /api/*. + useEffect(() => { + let cancelled = false; + const apiUrl = import.meta.env.VITE_API_URL ?? ''; + if (!apiUrl) return; + + fetch(`${apiUrl}/api/config/widget`) + .then(r => (r.ok ? r.json() : null)) + .then((cfg: { enabled?: boolean; key?: string; url?: string; embed?: { loginPage?: boolean } } | null) => { + if (cancelled || !cfg) return; + if (!cfg.enabled || !cfg.embed?.loginPage || !cfg.key) return; + if (document.getElementById('helix-widget-script')) return; + + const host = cfg.url && cfg.url.length > 0 ? cfg.url : apiUrl; + const script = document.createElement('script'); + script.id = 'helix-widget-script'; + script.src = `${host}/widget.js`; + script.setAttribute('data-key', cfg.key); + document.body.appendChild(script); + }) + .catch(err => { + // Never block login on a widget config failure. + console.warn('[widget] config fetch failed', err); + }); + + return () => { + cancelled = true; + document.getElementById('helix-widget-script')?.remove(); + document.getElementById('helix-widget-host')?.remove(); + }; + }, []); + const saved = localStorage.getItem('helix_remember'); const savedCreds = saved ? JSON.parse(saved) : null;