feat: embed website widget on login page via admin config endpoint

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 17:33:53 +05:30
parent 82ec843c6c
commit 0f23e84737

View File

@@ -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;