@xiwebdesign/xicms-core 7.4.0 → 7.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/AdminApp.d.ts +12 -1
- package/dist/admin/AdminApp.js +18 -14
- package/dist/admin/AdminApp.js.map +1 -1
- package/dist/admin/NewsEditor.js +15 -4
- package/dist/admin/NewsEditor.js.map +1 -1
- package/dist/admin/menu/MenuPanel.d.ts +20 -0
- package/dist/admin/menu/MenuPanel.js +144 -0
- package/dist/admin/menu/MenuPanel.js.map +1 -0
- package/dist/admin/panelRegistry.d.ts +10 -0
- package/dist/admin/panelRegistry.js +5 -1
- package/dist/admin/panelRegistry.js.map +1 -1
- package/dist/admin/settings/SettingsPanel.js +10 -2
- package/dist/admin/settings/SettingsPanel.js.map +1 -1
- package/dist/admin/ui/dialog.js +6 -3
- package/dist/admin/ui/dialog.js.map +1 -1
- package/dist/admin/ui/toast.d.ts +16 -0
- package/dist/admin/ui/toast.js +8 -4
- package/dist/admin/ui/toast.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +89 -123
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/auth/AuthProvider.js +11 -5
- package/dist/auth/AuthProvider.js.map +1 -1
- package/dist/auth/LoginForm.js +2 -1
- package/dist/auth/LoginForm.js.map +1 -1
- package/dist/components/ExternEmbed.d.ts +23 -0
- package/dist/components/ExternEmbed.js +52 -0
- package/dist/components/ExternEmbed.js.map +1 -0
- package/dist/editor/EditorProvider.d.ts +8 -0
- package/dist/editor/EditorProvider.js +30 -6
- package/dist/editor/EditorProvider.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/next/authRewrites.d.ts +29 -0
- package/dist/next/authRewrites.js +11 -0
- package/dist/next/authRewrites.js.map +1 -1
- package/dist/next/index.cjs +12 -0
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +2 -1
- package/dist/next/index.js.map +1 -1
- package/dist/render/PublishedPage.js +9 -1
- package/dist/render/PublishedPage.js.map +1 -1
- package/dist/schema/settings.d.ts +62 -1
- package/dist/schema/settings.js +23 -1
- package/dist/schema/settings.js.map +1 -1
- package/dist/schema/siteConfig.d.ts +5 -0
- package/dist/settings/useSiteSettings.js +1 -1
- package/dist/settings/useSiteSettings.js.map +1 -1
- package/dist/styles.css +67 -12
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -29,6 +29,7 @@ function AuthProvider({
|
|
|
29
29
|
const [demoLoggedIn, setDemoLoggedInState] = useState(false);
|
|
30
30
|
const [linkVraagtEmail, setLinkVraagtEmail] = useState(false);
|
|
31
31
|
const [linkFout, setLinkFout] = useState(null);
|
|
32
|
+
const [wakker, setWakker] = useState(0);
|
|
32
33
|
function opruimenNaLinkLogin() {
|
|
33
34
|
try {
|
|
34
35
|
window.localStorage.removeItem(LINK_EMAIL_KEY);
|
|
@@ -41,6 +42,10 @@ function AuthProvider({
|
|
|
41
42
|
} catch {
|
|
42
43
|
}
|
|
43
44
|
}
|
|
45
|
+
function wekOp() {
|
|
46
|
+
setAuthHint(true);
|
|
47
|
+
setWakker((n) => n + 1);
|
|
48
|
+
}
|
|
44
49
|
useEffect(() => {
|
|
45
50
|
if (!demo) return;
|
|
46
51
|
const sync = () => setDemoLoggedInState(isDemoLoggedIn());
|
|
@@ -110,7 +115,7 @@ function AuthProvider({
|
|
|
110
115
|
afgebroken = true;
|
|
111
116
|
unsub?.();
|
|
112
117
|
};
|
|
113
|
-
}, [configured]);
|
|
118
|
+
}, [configured, wakker]);
|
|
114
119
|
const value = useMemo(() => {
|
|
115
120
|
const demoActiveLogin = demo && demoLoggedIn;
|
|
116
121
|
const effectiveUser = demoActiveLogin ? { uid: "demo", email: "demo@voorbeeld.nl", displayName: "Demo-beheerder" } : user;
|
|
@@ -124,17 +129,18 @@ function AuthProvider({
|
|
|
124
129
|
async signIn(email, password) {
|
|
125
130
|
const { auth, sdk } = await authSdk();
|
|
126
131
|
await sdk.signInWithEmailAndPassword(auth, email, password);
|
|
127
|
-
|
|
132
|
+
wekOp();
|
|
128
133
|
},
|
|
129
134
|
async signInWithGoogle() {
|
|
130
135
|
const { auth, sdk } = await authSdk();
|
|
131
136
|
const provider = new sdk.GoogleAuthProvider();
|
|
137
|
+
wekOp();
|
|
132
138
|
try {
|
|
133
139
|
await sdk.signInWithPopup(auth, provider);
|
|
134
|
-
setAuthHint(true);
|
|
135
140
|
} catch (e) {
|
|
136
141
|
const code = e?.code ?? "";
|
|
137
|
-
if (code
|
|
142
|
+
if (code !== "auth/popup-blocked") throw e;
|
|
143
|
+
setAuthHint(true);
|
|
138
144
|
await sdk.signInWithRedirect(auth, provider);
|
|
139
145
|
}
|
|
140
146
|
},
|
|
@@ -154,7 +160,7 @@ function AuthProvider({
|
|
|
154
160
|
const { auth, sdk } = await authSdk();
|
|
155
161
|
await sdk.signInWithEmailLink(auth, email.trim(), window.location.href);
|
|
156
162
|
opruimenNaLinkLogin();
|
|
157
|
-
|
|
163
|
+
wekOp();
|
|
158
164
|
},
|
|
159
165
|
async signOut() {
|
|
160
166
|
if (demo) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/AuthProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from \"react\";\nimport type { User } from \"firebase/auth\";\nimport { hasAuthHint, isFirebaseConfigured, setAuthHint } from \"../firebase/config\";\nimport { roleForSite, type Role, type XiClaims } from \"./roles\";\nimport { DEMO_EVENT, isDemoLoggedIn, setDemoLoggedIn } from \"../demo/demoStore\";\n\ninterface AuthState {\n /** Of Firebase überhaupt geconfigureerd is. */\n configured: boolean;\n /** Bezig met laden van de auth-status. */\n loading: boolean;\n user: User | null;\n claims: XiClaims | null;\n /** Effectieve rol op de huidige site (uit `siteId`). */\n role: Role | null;\n signIn: (email: string, password: string) => Promise<void>;\n /** Inloggen met Google (popup). */\n signInWithGoogle: () => Promise<void>;\n /**\n * Stuur een inloglink naar dit adres (magic link). Geen wachtwoord, dus ook niets te vergeten —\n * en anders dan de Google-flow raakt dit geen enkel domein van een derde partij, waardoor het\n * werkt in browsers die opslag van derden blokkeren.\n */\n sendLoginLink: (email: string) => Promise<void>;\n /**\n * Er staat een geldige inloglink in de adresbalk, maar het bijbehorende adres is hier niet\n * bekend — de link is op een ander apparaat geopend dan waar hij is aangevraagd. Het\n * inlogscherm vraagt er dan om; zonder die vraag zou een geldige link doodlopen.\n */\n loginLinkNeedsEmail: boolean;\n /** De link inwisselen met een handmatig opgegeven adres (zie `loginLinkNeedsEmail`). */\n completeLoginLink: (email: string) => Promise<void>;\n /** Ging het inwisselen van de link mis (verlopen, al gebruikt, ander adres)? */\n loginLinkError: unknown;\n signOut: () => Promise<void>;\n /** Forceer het opnieuw inlezen van claims (na een rolwijziging). */\n refresh: () => Promise<void>;\n /** Demo-bewerkmodus actief (config.demoEditing) — pseudo-login, niets wordt opgeslagen. */\n demo: boolean;\n /** Pseudo-login voor de demo: geeft lokaal een siteadmin-rol zonder Firebase. */\n demoSignIn: () => void;\n}\n\nconst AuthContext = createContext<AuthState | null>(null);\n\n/**\n * Het adres waar de inloglink heen ging, bewaard tot de klik terugkomt.\n *\n * Firebase eist bij het afronden hetzelfde adres als waar de link naartoe is gestuurd — dat is de\n * hele beveiliging van deze methode: wie de link onderschept maar het adres niet weet, komt er\n * niet in. Staat het er niet (link geopend op een ander apparaat, of opslag geweigerd), dan vraagt\n * het inlogscherm er alsnog om in plaats van te falen.\n */\nconst LINK_EMAIL_KEY = \"xicms.loginlink.email\";\n\n/**\n * Is deze URL een inloglink? Bewust ZONDER de SDK: deze vraag bepaalt óf we de auth-SDK van ~1 MB\n * gaan laden, dus hem beantwoorden met de SDK zou de besparing meteen opeten. Firebase hangt de\n * link altijd op aan `mode=signIn` met een `oobCode`; dat is genoeg voor de voorselectie, waarna\n * `isSignInWithEmailLink` het echte oordeel velt.\n */\nfunction lijktOpInloglink(): boolean {\n if (typeof window === \"undefined\") return false;\n const q = window.location.search;\n return q.includes(\"mode=signIn\") && q.includes(\"oobCode=\");\n}\n\nexport function AuthProvider({\n siteId,\n demo = false,\n children,\n}: {\n siteId: string;\n demo?: boolean;\n children: ReactNode;\n}) {\n const configured = isFirebaseConfigured();\n const [loading, setLoading] = useState(configured);\n const [user, setUser] = useState<User | null>(null);\n const [claims, setClaims] = useState<XiClaims | null>(null);\n const [demoLoggedIn, setDemoLoggedInState] = useState(false);\n // Er staat een geldige inloglink in de adresbalk, maar we weten niet bij welk adres hij hoort.\n const [linkVraagtEmail, setLinkVraagtEmail] = useState(false);\n const [linkFout, setLinkFout] = useState<unknown>(null);\n\n /**\n * Na een geslaagde link-login: het bewaarde adres weg, en de `oobCode` uit de adresbalk.\n *\n * Die code is eenmalig, maar hij blijft wél in de geschiedenis en in gedeelde/gekopieerde URL's\n * staan. Hem laten staan levert bovendien een verwarrende herhaling op: één keer verversen en de\n * SDK probeert een verbruikte code opnieuw in te wisselen, met een foutmelding terwijl je gewoon\n * ingelogd bent. `replaceState` haalt hem weg zonder de pagina opnieuw te laden.\n */\n function opruimenNaLinkLogin(): void {\n try {\n window.localStorage.removeItem(LINK_EMAIL_KEY);\n } catch {\n /* niets bewaard, niets op te ruimen */\n }\n setLinkVraagtEmail(false);\n setLinkFout(null);\n try {\n window.history.replaceState({}, \"\", window.location.pathname + window.location.hash);\n } catch {\n /* geschiedenis niet bewerkbaar: onschuldig, de login is al gelukt */\n }\n }\n\n // Demo-login uit de (sessie)opslag inlezen + meeluisteren met wijzigingen (login/logout/reset).\n useEffect(() => {\n if (!demo) return;\n const sync = () => setDemoLoggedInState(isDemoLoggedIn());\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }, [demo]);\n\n /** Laadt de auth-SDK pas wanneer er iets mee moet gebeuren (inloggen, uitloggen, verversen). */\n async function authSdk() {\n const [client, sdk] = await Promise.all([import(\"../firebase/client\"), import(\"firebase/auth\")]);\n const auth = client.getFirebaseAuth();\n if (!auth) throw new Error(\"Firebase is niet geconfigureerd.\");\n return { auth, sdk };\n }\n\n async function loadClaims(u: User | null, forceRefresh = false) {\n if (!u) {\n setClaims(null);\n return;\n }\n // Standaard GEEN geforceerde token-refresh: dat is een netwerk-roundtrip op élke\n // auth-state-change (incl. eerste load van elke ingelogde editor). Alleen refresh() —\n // na een rolwijziging — forceert, want dan is het gecachte token per definitie verouderd.\n const token = await u.getIdTokenResult(forceRefresh);\n setClaims({\n superadmin: token.claims.superadmin === true,\n roles: (token.claims.roles as XiClaims[\"roles\"]) ?? {},\n });\n }\n\n useEffect(() => {\n // Zonder sessie-hint laden we de auth-SDK NIET: een anonieme bezoeker hoeft ~1 MB niet te\n // downloaden om te ontdekken dat hij niet is ingelogd. Zodra hij inlogt of het beheer opent\n // komt de SDK alsnog, en vindt onAuthStateChanged een bestaande sessie meteen terug.\n // …behalve als dit een inloglink IS. Wie op de link in zijn mail klikt komt per definitie\n // zonder sessie-hint binnen — vaak zelfs in een andere browser dan waar hij hem aanvroeg.\n // Zonder deze uitzondering laadt de SDK niet, wordt de link nooit ingewisseld en belandt hij\n // op het inlogscherm met de geldige link ongebruikt in de adresbalk.\n if (!configured || (!hasAuthHint() && !lijktOpInloglink())) {\n setLoading(false);\n return;\n }\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const { auth, sdk } = await authSdk().catch(() => ({ auth: null, sdk: null }) as never);\n if (!auth || afgebroken) {\n setLoading(false);\n return;\n }\n // Terugkeer van een redirect-login afronden (zie signInWithGoogle). `onAuthStateChanged`\n // pikt de sessie zelf al op; deze aanroep is er om een FOUT bij terugkomst niet stil te\n // laten verdwijnen — anders sta je weer op het inlogscherm zonder te weten waarom.\n void sdk.getRedirectResult(auth).catch((e: unknown) => {\n console.error(\"[xicms] inloggen via redirect mislukt:\", e);\n });\n\n // Een aangeklikte inloglink meteen inwisselen. Het adres staat lokaal opgeslagen sinds het\n // versturen; ontbreekt het — link geopend op een ander apparaat, of opslag geweigerd — dan\n // laten we de link stáán en vraagt het inlogscherm erom. Weggooien zou de gebruiker terug\n // naar zijn mailbox sturen voor een link die hij al heeft.\n if (sdk.isSignInWithEmailLink(auth, window.location.href)) {\n let bewaard: string | null = null;\n try {\n bewaard = window.localStorage.getItem(LINK_EMAIL_KEY);\n } catch {\n /* opslag geweigerd → hieronder vragen we het adres gewoon */\n }\n if (bewaard) {\n try {\n await sdk.signInWithEmailLink(auth, bewaard, window.location.href);\n opruimenNaLinkLogin();\n } catch (e) {\n console.error(\"[xicms] inloggen via link mislukt:\", e);\n setLinkFout(e);\n }\n } else if (!afgebroken) {\n setLinkVraagtEmail(true);\n }\n }\n\n unsub = sdk.onAuthStateChanged(auth, async (u: User | null) => {\n setUser(u);\n setAuthHint(Boolean(u));\n await loadClaims(u);\n setLoading(false);\n });\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [configured]);\n\n const value = useMemo<AuthState>(() => {\n // In de demo met actieve pseudo-login: doe alsof een siteadmin is ingelogd (lokaal, geen Firebase).\n const demoActiveLogin = demo && demoLoggedIn;\n const effectiveUser = demoActiveLogin\n ? ({ uid: \"demo\", email: \"demo@voorbeeld.nl\", displayName: \"Demo-beheerder\" } as unknown as User)\n : user;\n const effectiveClaims: XiClaims | null = demoActiveLogin ? { roles: { [siteId]: \"siteadmin\" } } : claims;\n\n return {\n configured,\n loading,\n user: effectiveUser,\n claims: effectiveClaims,\n role: roleForSite(effectiveClaims, siteId),\n async signIn(email, password) {\n const { auth, sdk } = await authSdk();\n await sdk.signInWithEmailAndPassword(auth, email, password);\n setAuthHint(true);\n },\n async signInWithGoogle() {\n const { auth, sdk } = await authSdk();\n const provider = new sdk.GoogleAuthProvider();\n try {\n await sdk.signInWithPopup(auth, provider);\n setAuthHint(true);\n } catch (e) {\n // TERUGVAL OP REDIRECT. `signInWithPopup` faalt in de praktijk vaker dan je denkt: een\n // popup-blokkade, een browser die de popup als tab opent (dan raakt Firebase het\n // venster kwijt), of strengere cookie-instellingen. Gemeten op localhost: de popup ging\n // dicht, de gebruiker bleef uitgelogd, en er kwam GEEN foutcode — waardoor het\n // inlogscherm alleen \"mislukt of geannuleerd\" kon melden. Doodlopend voor de klant.\n //\n // De redirect-variant heeft geen venster nodig en werkt daarom overal; het resultaat\n // wordt bij terugkomst opgepikt (zie de getRedirectResult-effect hieronder). Alleen bij\n // een échte annulering door de gebruiker gooien we door — dan opnieuw beginnen is\n // precies niet wat hij wil.\n const code = (e as { code?: string })?.code ?? \"\";\n if (code === \"auth/cancelled-popup-request\" || code === \"auth/popup-closed-by-user\") throw e;\n await sdk.signInWithRedirect(auth, provider);\n }\n },\n async sendLoginLink(email: string) {\n const { auth, sdk } = await authSdk();\n const adres = email.trim();\n // De link komt terug op DEZE pagina, zodat de gebruiker na het inloggen staat waar hij\n // begon (meestal /login of /beheer) in plaats van op de voorpagina.\n const url = `${window.location.origin}${window.location.pathname}`;\n await sdk.sendSignInLinkToEmail(auth, adres, { url, handleCodeInApp: true });\n try {\n window.localStorage.setItem(LINK_EMAIL_KEY, adres);\n } catch {\n // Opslag geweigerd (private mode): niet fataal. Bij terugkomst vraagt het inlogscherm\n // het adres alsnog — dezelfde route als bij een link die op een ander apparaat opengaat.\n }\n },\n loginLinkNeedsEmail: linkVraagtEmail,\n loginLinkError: linkFout,\n async completeLoginLink(email: string) {\n const { auth, sdk } = await authSdk();\n await sdk.signInWithEmailLink(auth, email.trim(), window.location.href);\n opruimenNaLinkLogin();\n setAuthHint(true);\n },\n async signOut() {\n if (demo) {\n setDemoLoggedIn(false);\n return;\n }\n const { auth, sdk } = await authSdk();\n await sdk.signOut(auth);\n setAuthHint(false);\n },\n async refresh() {\n // Altijd de actuele Firebase-user (niet de gememoïseerde) — na een snelle\n // uitlog/inlog-wissel zou de closure anders claims van de vorige user verversen.\n // Force-refresh: na een rolwijziging is het gecachte token verouderd.\n const { auth } = await authSdk();\n await loadClaims(auth.currentUser ?? user, true);\n },\n demo,\n demoSignIn() {\n setDemoLoggedIn(true);\n },\n };\n }, [configured, loading, user, claims, siteId, demo, demoLoggedIn, linkVraagtEmail, linkFout]);\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\nexport function useAuth(): AuthState {\n const ctx = useContext(AuthContext);\n if (!ctx) throw new Error(\"useAuth moet binnen <XiCMSProvider> worden gebruikt.\");\n return ctx;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/AuthProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from \"react\";\nimport type { User } from \"firebase/auth\";\nimport { hasAuthHint, isFirebaseConfigured, setAuthHint } from \"../firebase/config\";\nimport { roleForSite, type Role, type XiClaims } from \"./roles\";\nimport { DEMO_EVENT, isDemoLoggedIn, setDemoLoggedIn } from \"../demo/demoStore\";\n\ninterface AuthState {\n /** Of Firebase überhaupt geconfigureerd is. */\n configured: boolean;\n /** Bezig met laden van de auth-status. */\n loading: boolean;\n user: User | null;\n claims: XiClaims | null;\n /** Effectieve rol op de huidige site (uit `siteId`). */\n role: Role | null;\n signIn: (email: string, password: string) => Promise<void>;\n /** Inloggen met Google (popup). */\n signInWithGoogle: () => Promise<void>;\n /**\n * Stuur een inloglink naar dit adres (magic link). Geen wachtwoord, dus ook niets te vergeten —\n * en anders dan de Google-flow raakt dit geen enkel domein van een derde partij, waardoor het\n * werkt in browsers die opslag van derden blokkeren.\n */\n sendLoginLink: (email: string) => Promise<void>;\n /**\n * Er staat een geldige inloglink in de adresbalk, maar het bijbehorende adres is hier niet\n * bekend — de link is op een ander apparaat geopend dan waar hij is aangevraagd. Het\n * inlogscherm vraagt er dan om; zonder die vraag zou een geldige link doodlopen.\n */\n loginLinkNeedsEmail: boolean;\n /** De link inwisselen met een handmatig opgegeven adres (zie `loginLinkNeedsEmail`). */\n completeLoginLink: (email: string) => Promise<void>;\n /** Ging het inwisselen van de link mis (verlopen, al gebruikt, ander adres)? */\n loginLinkError: unknown;\n signOut: () => Promise<void>;\n /** Forceer het opnieuw inlezen van claims (na een rolwijziging). */\n refresh: () => Promise<void>;\n /** Demo-bewerkmodus actief (config.demoEditing) — pseudo-login, niets wordt opgeslagen. */\n demo: boolean;\n /** Pseudo-login voor de demo: geeft lokaal een siteadmin-rol zonder Firebase. */\n demoSignIn: () => void;\n}\n\nconst AuthContext = createContext<AuthState | null>(null);\n\n/**\n * Het adres waar de inloglink heen ging, bewaard tot de klik terugkomt.\n *\n * Firebase eist bij het afronden hetzelfde adres als waar de link naartoe is gestuurd — dat is de\n * hele beveiliging van deze methode: wie de link onderschept maar het adres niet weet, komt er\n * niet in. Staat het er niet (link geopend op een ander apparaat, of opslag geweigerd), dan vraagt\n * het inlogscherm er alsnog om in plaats van te falen.\n */\nconst LINK_EMAIL_KEY = \"xicms.loginlink.email\";\n\n/**\n * Is deze URL een inloglink? Bewust ZONDER de SDK: deze vraag bepaalt óf we de auth-SDK van ~1 MB\n * gaan laden, dus hem beantwoorden met de SDK zou de besparing meteen opeten. Firebase hangt de\n * link altijd op aan `mode=signIn` met een `oobCode`; dat is genoeg voor de voorselectie, waarna\n * `isSignInWithEmailLink` het echte oordeel velt.\n */\nfunction lijktOpInloglink(): boolean {\n if (typeof window === \"undefined\") return false;\n const q = window.location.search;\n return q.includes(\"mode=signIn\") && q.includes(\"oobCode=\");\n}\n\nexport function AuthProvider({\n siteId,\n demo = false,\n children,\n}: {\n siteId: string;\n demo?: boolean;\n children: ReactNode;\n}) {\n const configured = isFirebaseConfigured();\n const [loading, setLoading] = useState(configured);\n const [user, setUser] = useState<User | null>(null);\n const [claims, setClaims] = useState<XiClaims | null>(null);\n const [demoLoggedIn, setDemoLoggedInState] = useState(false);\n // Er staat een geldige inloglink in de adresbalk, maar we weten niet bij welk adres hij hoort.\n const [linkVraagtEmail, setLinkVraagtEmail] = useState(false);\n const [linkFout, setLinkFout] = useState<unknown>(null);\n // Alleen om het abonnement-effect opnieuw te laten draaien na een login zonder paginawissel.\n const [wakker, setWakker] = useState(0);\n\n /**\n * Na een geslaagde link-login: het bewaarde adres weg, en de `oobCode` uit de adresbalk.\n *\n * Die code is eenmalig, maar hij blijft wél in de geschiedenis en in gedeelde/gekopieerde URL's\n * staan. Hem laten staan levert bovendien een verwarrende herhaling op: één keer verversen en de\n * SDK probeert een verbruikte code opnieuw in te wisselen, met een foutmelding terwijl je gewoon\n * ingelogd bent. `replaceState` haalt hem weg zonder de pagina opnieuw te laden.\n */\n function opruimenNaLinkLogin(): void {\n try {\n window.localStorage.removeItem(LINK_EMAIL_KEY);\n } catch {\n /* niets bewaard, niets op te ruimen */\n }\n setLinkVraagtEmail(false);\n setLinkFout(null);\n try {\n window.history.replaceState({}, \"\", window.location.pathname + window.location.hash);\n } catch {\n /* geschiedenis niet bewerkbaar: onschuldig, de login is al gelukt */\n }\n }\n\n /**\n * Zet de sessie-hint ÉN maak het effect hieronder wakker.\n *\n * Dat tweede is de kern van een bug die zich als \"ik kom er niet in\" voordeed. Het effect dat\n * `onAuthStateChanged` abonneert draait alleen als er bij het LADEN van de pagina al een hint is,\n * en hangt af van `[configured]` — een hint die later wordt gezet laat het dus niet opnieuw\n * draaien. Gevolg: log je in zónder paginawissel (popup of wachtwoord), dan maakt Firebase keurig\n * een sessie, maar luistert er niemand. React hoort het nooit en het inlogscherm blijft staan.\n *\n * Dat het toch vaak leek te werken kwam door de redirect-terugval: die verlaat de pagina en komt\n * terug op een verse load, waar de hint er wél staat. Precies daarom viel dit pas op ná het\n * repareren van de popup — toen verdween de herlaad die het gebrek maskeerde.\n *\n * De teller hoeft nergens te worden uitgelezen; hij bestaat alleen om de afhankelijkheidslijst\n * te laten wijzigen.\n */\n function wekOp(): void {\n setAuthHint(true);\n setWakker((n) => n + 1);\n }\n\n // Demo-login uit de (sessie)opslag inlezen + meeluisteren met wijzigingen (login/logout/reset).\n useEffect(() => {\n if (!demo) return;\n const sync = () => setDemoLoggedInState(isDemoLoggedIn());\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }, [demo]);\n\n /** Laadt de auth-SDK pas wanneer er iets mee moet gebeuren (inloggen, uitloggen, verversen). */\n async function authSdk() {\n const [client, sdk] = await Promise.all([import(\"../firebase/client\"), import(\"firebase/auth\")]);\n const auth = client.getFirebaseAuth();\n if (!auth) throw new Error(\"Firebase is niet geconfigureerd.\");\n return { auth, sdk };\n }\n\n async function loadClaims(u: User | null, forceRefresh = false) {\n if (!u) {\n setClaims(null);\n return;\n }\n // Standaard GEEN geforceerde token-refresh: dat is een netwerk-roundtrip op élke\n // auth-state-change (incl. eerste load van elke ingelogde editor). Alleen refresh() —\n // na een rolwijziging — forceert, want dan is het gecachte token per definitie verouderd.\n const token = await u.getIdTokenResult(forceRefresh);\n setClaims({\n superadmin: token.claims.superadmin === true,\n roles: (token.claims.roles as XiClaims[\"roles\"]) ?? {},\n });\n }\n\n useEffect(() => {\n // Zonder sessie-hint laden we de auth-SDK NIET: een anonieme bezoeker hoeft ~1 MB niet te\n // downloaden om te ontdekken dat hij niet is ingelogd. Zodra hij inlogt of het beheer opent\n // komt de SDK alsnog, en vindt onAuthStateChanged een bestaande sessie meteen terug.\n // …behalve als dit een inloglink IS. Wie op de link in zijn mail klikt komt per definitie\n // zonder sessie-hint binnen — vaak zelfs in een andere browser dan waar hij hem aanvroeg.\n // Zonder deze uitzondering laadt de SDK niet, wordt de link nooit ingewisseld en belandt hij\n // op het inlogscherm met de geldige link ongebruikt in de adresbalk.\n if (!configured || (!hasAuthHint() && !lijktOpInloglink())) {\n setLoading(false);\n return;\n }\n let unsub: (() => void) | undefined;\n let afgebroken = false;\n void (async () => {\n const { auth, sdk } = await authSdk().catch(() => ({ auth: null, sdk: null }) as never);\n if (!auth || afgebroken) {\n setLoading(false);\n return;\n }\n // Terugkeer van een redirect-login afronden (zie signInWithGoogle). `onAuthStateChanged`\n // pikt de sessie zelf al op; deze aanroep is er om een FOUT bij terugkomst niet stil te\n // laten verdwijnen — anders sta je weer op het inlogscherm zonder te weten waarom.\n void sdk.getRedirectResult(auth).catch((e: unknown) => {\n console.error(\"[xicms] inloggen via redirect mislukt:\", e);\n });\n\n // Een aangeklikte inloglink meteen inwisselen. Het adres staat lokaal opgeslagen sinds het\n // versturen; ontbreekt het — link geopend op een ander apparaat, of opslag geweigerd — dan\n // laten we de link stáán en vraagt het inlogscherm erom. Weggooien zou de gebruiker terug\n // naar zijn mailbox sturen voor een link die hij al heeft.\n if (sdk.isSignInWithEmailLink(auth, window.location.href)) {\n let bewaard: string | null = null;\n try {\n bewaard = window.localStorage.getItem(LINK_EMAIL_KEY);\n } catch {\n /* opslag geweigerd → hieronder vragen we het adres gewoon */\n }\n if (bewaard) {\n try {\n await sdk.signInWithEmailLink(auth, bewaard, window.location.href);\n opruimenNaLinkLogin();\n } catch (e) {\n console.error(\"[xicms] inloggen via link mislukt:\", e);\n setLinkFout(e);\n }\n } else if (!afgebroken) {\n setLinkVraagtEmail(true);\n }\n }\n\n unsub = sdk.onAuthStateChanged(auth, async (u: User | null) => {\n setUser(u);\n setAuthHint(Boolean(u));\n await loadClaims(u);\n setLoading(false);\n });\n })();\n return () => {\n afgebroken = true;\n unsub?.();\n };\n }, [configured, wakker]);\n\n const value = useMemo<AuthState>(() => {\n // In de demo met actieve pseudo-login: doe alsof een siteadmin is ingelogd (lokaal, geen Firebase).\n const demoActiveLogin = demo && demoLoggedIn;\n const effectiveUser = demoActiveLogin\n ? ({ uid: \"demo\", email: \"demo@voorbeeld.nl\", displayName: \"Demo-beheerder\" } as unknown as User)\n : user;\n const effectiveClaims: XiClaims | null = demoActiveLogin ? { roles: { [siteId]: \"siteadmin\" } } : claims;\n\n return {\n configured,\n loading,\n user: effectiveUser,\n claims: effectiveClaims,\n role: roleForSite(effectiveClaims, siteId),\n async signIn(email, password) {\n const { auth, sdk } = await authSdk();\n await sdk.signInWithEmailAndPassword(auth, email, password);\n wekOp();\n },\n async signInWithGoogle() {\n const { auth, sdk } = await authSdk();\n const provider = new sdk.GoogleAuthProvider();\n /**\n * Abonneren VÓÓR de popup opengaat, niet erna.\n *\n * `signInWithPopup` lost pas op als Firebase de hele tokenuitwisseling heeft afgerond —\n * `signInWithIdp`, dan `lookup` — en pas dán sluit de handler zijn eigen venster. Wachtte de\n * UI op die belofte, dan bleef het inlogscherm al die tijd staan terwijl de sessie er vaak\n * al lang was. Voor de gebruiker las dat als \"er gebeurt niets\".\n *\n * Met de luisteraar al actief reageert het scherm op het moment dat de sessie ontstaat, niet\n * op het moment dat Firebase klaar is met opruimen. Het venster sluit daarna alsnog wat later\n * — dat is Firebase' eigen tijd en die willen we niet inkorten, want dan sluiten we het\n * midden in de tokenuitwisseling.\n */\n wekOp();\n try {\n await sdk.signInWithPopup(auth, provider);\n } catch (e) {\n // TERUGVAL OP REDIRECT — maar ALLEEN als de popup niet eens open kón.\n //\n // Dit stond hier breed: elke fout behalve annuleren stuurde de hele pagina naar Google.\n // Dat is destructief zodra de popup het wél doet, en precies dat gebeurde. Waargenomen op\n // christelijkkraamcadeau: je koos je account in de popup, en tegelijk opende de\n // ONDERLIGGENDE beheerpagina een tweede Google-scherm. In de console was ondertussen te\n // zien dat `accounts:signInWithIdp` en `accounts:lookup` gewoon slaagden — Firebase had je\n // dus allang ingelogd. Twee flows die door elkaar heen liepen, met \"Inloggen met Google\n // mislukt of geannuleerd\" als eindresultaat terwijl er niets mislukt was.\n //\n // Een redirect gooit de hele pagina weg. Dat is een zwaar middel en hoort alleen bij de\n // ene fout waarbij er echt geen andere weg is: de browser weigerde het venster te openen.\n // Alle andere fouten — netwerk, configuratie, een afgebroken relay — horen zichtbaar te\n // falen, niet stilzwijgend een tweede inlogpoging te starten.\n //\n // De aanleiding voor de brede terugval was een popup die dichtging zonder foutcode. Die\n // kwam door de COOP-blokkade (zie next/authRewrites.ts), en die is nu bij de bron\n // opgelost. Daarmee vervalt de reden om hier ruim te zijn.\n // Merk op dat hier GEEN `wekOp()` meer staat: de luisteraar is al vóór de popup\n // geabonneerd. Dat dekt meteen het geval dat de belofte afwijst terwijl er tóch een sessie\n // is — in de gemeten gevallen slaagden `signInWithIdp` en `lookup` gewoon terwijl deze\n // catch liep. `onAuthStateChanged` ziet die sessie dan alsnog en het inlogscherm verdwijnt.\n const code = (e as { code?: string })?.code ?? \"\";\n if (code !== \"auth/popup-blocked\") throw e;\n // ZET DE HINT VÓÓR HET VERTREK. Zonder deze regel heeft de redirect-terugval nog nooit\n // gewerkt voor een eerste login, en wel zo stil dat het op de popup werd geschoven. De\n // keten: je gaat naar Google, komt terug op een verse pagina, en daar besluit het effect\n // hierboven de auth-SDK NIET te laden — want er is geen sessie-hint (je was immers nog\n // nooit ingelogd) en het is geen inloglink. Laadt de SDK niet, dan draait\n // `getRedirectResult` niet, en wordt het antwoord van Google nooit opgehaald.\n setAuthHint(true);\n await sdk.signInWithRedirect(auth, provider);\n }\n },\n async sendLoginLink(email: string) {\n const { auth, sdk } = await authSdk();\n const adres = email.trim();\n // De link komt terug op DEZE pagina, zodat de gebruiker na het inloggen staat waar hij\n // begon (meestal /login of /beheer) in plaats van op de voorpagina.\n const url = `${window.location.origin}${window.location.pathname}`;\n await sdk.sendSignInLinkToEmail(auth, adres, { url, handleCodeInApp: true });\n try {\n window.localStorage.setItem(LINK_EMAIL_KEY, adres);\n } catch {\n // Opslag geweigerd (private mode): niet fataal. Bij terugkomst vraagt het inlogscherm\n // het adres alsnog — dezelfde route als bij een link die op een ander apparaat opengaat.\n }\n },\n loginLinkNeedsEmail: linkVraagtEmail,\n loginLinkError: linkFout,\n async completeLoginLink(email: string) {\n const { auth, sdk } = await authSdk();\n await sdk.signInWithEmailLink(auth, email.trim(), window.location.href);\n opruimenNaLinkLogin();\n wekOp();\n },\n async signOut() {\n if (demo) {\n setDemoLoggedIn(false);\n return;\n }\n const { auth, sdk } = await authSdk();\n await sdk.signOut(auth);\n setAuthHint(false);\n },\n async refresh() {\n // Altijd de actuele Firebase-user (niet de gememoïseerde) — na een snelle\n // uitlog/inlog-wissel zou de closure anders claims van de vorige user verversen.\n // Force-refresh: na een rolwijziging is het gecachte token verouderd.\n const { auth } = await authSdk();\n await loadClaims(auth.currentUser ?? user, true);\n },\n demo,\n demoSignIn() {\n setDemoLoggedIn(true);\n },\n };\n }, [configured, loading, user, claims, siteId, demo, demoLoggedIn, linkVraagtEmail, linkFout]);\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\nexport function useAuth(): AuthState {\n const ctx = useContext(AuthContext);\n if (!ctx) throw new Error(\"useAuth moet binnen <XiCMSProvider> worden gebruikt.\");\n return ctx;\n}\n"],"mappings":"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","names":[]}
|
package/dist/auth/LoginForm.js
CHANGED
|
@@ -23,9 +23,10 @@ function authErrorMessage(e, fallback) {
|
|
|
23
23
|
case "auth/invalid-email":
|
|
24
24
|
return "Inloggen mislukt. Controleer je e-mailadres en wachtwoord.";
|
|
25
25
|
case "auth/popup-blocked":
|
|
26
|
+
return "De browser blokkeerde het Google-venster. Sta popups toe voor deze site, of gebruik de inloglink hieronder.";
|
|
26
27
|
case "auth/popup-closed-by-user":
|
|
27
28
|
case "auth/cancelled-popup-request":
|
|
28
|
-
return "
|
|
29
|
+
return "Het Google-venster werd gesloten voordat het inloggen klaar was. Probeer het opnieuw, of gebruik de inloglink hieronder.";
|
|
29
30
|
// ── Inloglink ──────────────────────────────────────────────────────────────────────────
|
|
30
31
|
case "auth/expired-action-code":
|
|
31
32
|
return "Deze inloglink is verlopen. Vraag hieronder een nieuwe aan.";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/LoginForm.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState, type CSSProperties, type FormEvent } from \"react\";\nimport { useAuth } from \"./AuthProvider\";\n\n/**\n * Vertaalt een Firebase-auth-fout naar een bruikbare melding. Maakt de meest voorkomende\n * deploy-/configuratieproblemen zelf-diagnosticerend i.p.v. een generieke \"inloggen mislukt\".\n */\nfunction authErrorMessage(e: unknown, fallback: string): string {\n const code = (e as { code?: string })?.code ?? \"\";\n switch (code) {\n case \"auth/unauthorized-domain\":\n return \"Dit domein mag nog niet inloggen. Voeg het toe in Firebase → Authentication → Settings → Authorized domains (bv. je Vercel-domein).\";\n case \"auth/invalid-api-key\":\n case \"auth/api-key-not-valid\":\n case \"auth/api-key-not-valid.-please-pass-a-valid-api-key.\":\n return \"De Firebase API-sleutel klopt niet. Controleer NEXT_PUBLIC_FIREBASE_API_KEY in de (Vercel-)omgevingsvariabelen.\";\n case \"auth/configuration-not-found\":\n return \"Firebase Authentication is niet (juist) ingericht voor dit project. Controleer de NEXT_PUBLIC_FIREBASE_*-env en of Authentication aan staat.\";\n case \"auth/operation-not-allowed\":\n return \"Deze inlogmethode staat uit. Zet 'm aan in Firebase → Authentication → Sign-in method (Google of E-mailkoppeling).\";\n case \"auth/network-request-failed\":\n return \"Geen verbinding met Firebase. Controleer je netwerk en de NEXT_PUBLIC_FIREBASE_*-config.\";\n case \"auth/invalid-credential\":\n case \"auth/wrong-password\":\n case \"auth/user-not-found\":\n case \"auth/invalid-email\":\n return \"Inloggen mislukt. Controleer je e-mailadres en wachtwoord.\";\n case \"auth/popup-blocked\":\n case \"auth/popup-closed-by-user\":\n case \"auth/cancelled-popup-request\":\n return \"De Google-login-popup werd geblokkeerd of gesloten. Sta popups toe en probeer opnieuw.\";\n // ── Inloglink ──────────────────────────────────────────────────────────────────────────\n case \"auth/expired-action-code\":\n return \"Deze inloglink is verlopen. Vraag hieronder een nieuwe aan.\";\n case \"auth/invalid-action-code\":\n return \"Deze inloglink is al gebruikt of niet meer geldig. Vraag hieronder een nieuwe aan.\";\n default:\n return code ? `${fallback} (${code})` : fallback;\n }\n}\n\n/**\n * Inlogscherm voor beheerders/editors — wachtwoordloos van opzet.\n *\n * Twee wegen naar binnen: Google, of een inloglink per mail. Geen van beide kent een wachtwoord,\n * en dus ook geen vergeten-wachtwoord-mailtjes. De inloglink is bovendien de betrouwbaarste van de\n * twee: hij raakt geen enkel domein van een derde partij en werkt daarom ook in browsers die\n * opslag van derden blokkeren — precies waar de Google-redirect op stukliep.\n *\n * Het wachtwoordformulier is niet weggegooid maar weggevouwen. Als beide wachtwoordloze wegen over\n * e-mail lopen, is e-mail een enkelvoudig storingspunt: hapert de mailbezorging, dan komt niemand\n * er meer in — ook de beheerder niet. Deze terugval blijft daarom bestaan voor wie hem nodig heeft,\n * zonder hem aan iedereen op te dringen.\n */\nexport function LoginForm({ onSuccess }: { onSuccess?: () => void }) {\n const {\n signIn,\n signInWithGoogle,\n sendLoginLink,\n loginLinkNeedsEmail,\n loginLinkError,\n completeLoginLink,\n configured,\n user,\n } = useAuth();\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n const [linkVerstuurd, setLinkVerstuurd] = useState(false);\n const [wachtwoordModus, setWachtwoordModus] = useState(false);\n const [busy, setBusy] = useState(false);\n\n // Een mislukte link-inwisseling gebeurt in de provider (bij het laden), niet hier. Zonder deze\n // doorgifte zou de gebruiker een leeg inlogscherm zien terwijl er wél iets mis was.\n useEffect(() => {\n if (loginLinkError) setError(authErrorMessage(loginLinkError, \"Inloggen via de link mislukt.\"));\n }, [loginLinkError]);\n\n async function handleGoogle() {\n setError(null);\n setBusy(true);\n try {\n await signInWithGoogle();\n onSuccess?.();\n } catch (e) {\n setError(authErrorMessage(e, \"Inloggen met Google mislukt of geannuleerd.\"));\n } finally {\n setBusy(false);\n }\n }\n\n if (!configured) {\n return (\n <div className=\"xicms-card\" style={{ padding: \"1.5rem\" }}>\n <p>\n Firebase is nog niet geconfigureerd. Vul de <code>NEXT_PUBLIC_FIREBASE_*</code>\n omgevingsvariabelen in om inloggen aan te zetten.\n </p>\n </div>\n );\n }\n\n if (user) {\n return (\n <div className=\"xicms-card\" style={{ padding: \"1.5rem\" }}>\n <p>Je bent ingelogd als <strong>{user.email}</strong>.</p>\n </div>\n );\n }\n\n /** Verstuur de inloglink, of — bij terugkomst zonder bekend adres — wissel de link juist in. */\n async function handleEmail(e: FormEvent) {\n e.preventDefault();\n setError(null);\n setBusy(true);\n try {\n if (loginLinkNeedsEmail) {\n await completeLoginLink(email);\n onSuccess?.();\n } else {\n await sendLoginLink(email);\n setLinkVerstuurd(true);\n }\n } catch (err) {\n setError(authErrorMessage(err, \"Versturen van de inloglink mislukt.\"));\n } finally {\n setBusy(false);\n }\n }\n\n async function handleWachtwoord(e: FormEvent) {\n e.preventDefault();\n setError(null);\n setBusy(true);\n try {\n await signIn(email, password);\n onSuccess?.();\n } catch (err) {\n setError(authErrorMessage(err, \"Inloggen mislukt. Controleer je e-mailadres en wachtwoord.\"));\n } finally {\n setBusy(false);\n }\n }\n\n const kaart: CSSProperties = {\n padding: \"1.5rem\",\n display: \"grid\",\n gap: \"0.75rem\",\n maxWidth: 360,\n };\n\n // Bevestiging ná het versturen. Bewust geen formulier meer: de volgende stap staat in de mailbox,\n // en een nog zichtbaar veld nodigt uit tot nog een keer klikken (waarmee de eerste link vervalt).\n if (linkVerstuurd) {\n return (\n <div className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Kijk in je mail</h2>\n <p style={{ margin: 0 }}>\n We stuurden een inloglink naar <strong>{email}</strong>. Klik erop en je bent binnen — geen\n wachtwoord nodig.\n </p>\n <p style={{ margin: 0, fontSize: 13, opacity: 0.7 }}>\n Niets ontvangen? Kijk in je spam, of{\" \"}\n <button\n type=\"button\"\n onClick={() => setLinkVerstuurd(false)}\n className=\"xicms-btn xicms-btn--ghost\"\n style={{ padding: 0, border: 0, background: \"none\", textDecoration: \"underline\", cursor: \"pointer\" }}\n >\n probeer een ander adres\n </button>\n .\n </p>\n </div>\n );\n }\n\n if (wachtwoordModus) {\n return (\n <form onSubmit={handleWachtwoord} className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Inloggen met wachtwoord</h2>\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>E-mailadres</span>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"xicms-input\"\n />\n </label>\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>Wachtwoord</span>\n <input\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n autoComplete=\"current-password\"\n className=\"xicms-input\"\n />\n </label>\n {error && <p style={{ color: \"#dc2626\", margin: 0 }}>{error}</p>}\n <button type=\"submit\" disabled={busy} className=\"xicms-btn\">\n {busy ? \"Bezig…\" : \"Inloggen\"}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setWachtwoordModus(false);\n setError(null);\n }}\n className=\"xicms-btn xicms-btn--ghost\"\n >\n Liever zonder wachtwoord\n </button>\n </form>\n );\n }\n\n return (\n <form onSubmit={handleEmail} className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Inloggen</h2>\n\n {loginLinkNeedsEmail && (\n <p style={{ margin: 0, fontSize: 14 }}>\n Bevestig even bij welk e-mailadres deze inloglink hoort. Dat is wat de link beveiligt: wie\n hem onderschept maar het adres niet weet, komt er niet in.\n </p>\n )}\n\n <button type=\"button\" onClick={handleGoogle} disabled={busy} className=\"xicms-btn\">\n Inloggen met Google\n </button>\n\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"0.5rem\", opacity: 0.6, fontSize: 13 }}>\n <span style={{ flex: 1, height: 1, background: \"currentColor\" }} /> of{\" \"}\n <span style={{ flex: 1, height: 1, background: \"currentColor\" }} />\n </div>\n\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>E-mailadres</span>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"xicms-input\"\n />\n </label>\n {error && <p style={{ color: \"#dc2626\", margin: 0 }}>{error}</p>}\n <button type=\"submit\" disabled={busy} className=\"xicms-btn xicms-btn--ghost\">\n {busy ? \"Bezig…\" : loginLinkNeedsEmail ? \"Inloggen afronden\" : \"Stuur mij een inloglink\"}\n </button>\n\n {/* De terugval: aanwezig voor wie hem nodig heeft, stil voor wie dat niet heeft. */}\n {!loginLinkNeedsEmail && (\n <button\n type=\"button\"\n onClick={() => {\n setWachtwoordModus(true);\n setError(null);\n }}\n style={{\n justifySelf: \"start\",\n padding: 0,\n border: 0,\n background: \"none\",\n font: \"inherit\",\n fontSize: 12,\n opacity: 0.55,\n textDecoration: \"underline\",\n cursor: \"pointer\",\n }}\n >\n Inloggen met wachtwoord\n </button>\n )}\n </form>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/LoginForm.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState, type CSSProperties, type FormEvent } from \"react\";\nimport { useAuth } from \"./AuthProvider\";\n\n/**\n * Vertaalt een Firebase-auth-fout naar een bruikbare melding. Maakt de meest voorkomende\n * deploy-/configuratieproblemen zelf-diagnosticerend i.p.v. een generieke \"inloggen mislukt\".\n */\nfunction authErrorMessage(e: unknown, fallback: string): string {\n const code = (e as { code?: string })?.code ?? \"\";\n switch (code) {\n case \"auth/unauthorized-domain\":\n return \"Dit domein mag nog niet inloggen. Voeg het toe in Firebase → Authentication → Settings → Authorized domains (bv. je Vercel-domein).\";\n case \"auth/invalid-api-key\":\n case \"auth/api-key-not-valid\":\n case \"auth/api-key-not-valid.-please-pass-a-valid-api-key.\":\n return \"De Firebase API-sleutel klopt niet. Controleer NEXT_PUBLIC_FIREBASE_API_KEY in de (Vercel-)omgevingsvariabelen.\";\n case \"auth/configuration-not-found\":\n return \"Firebase Authentication is niet (juist) ingericht voor dit project. Controleer de NEXT_PUBLIC_FIREBASE_*-env en of Authentication aan staat.\";\n case \"auth/operation-not-allowed\":\n return \"Deze inlogmethode staat uit. Zet 'm aan in Firebase → Authentication → Sign-in method (Google of E-mailkoppeling).\";\n case \"auth/network-request-failed\":\n return \"Geen verbinding met Firebase. Controleer je netwerk en de NEXT_PUBLIC_FIREBASE_*-config.\";\n case \"auth/invalid-credential\":\n case \"auth/wrong-password\":\n case \"auth/user-not-found\":\n case \"auth/invalid-email\":\n return \"Inloggen mislukt. Controleer je e-mailadres en wachtwoord.\";\n case \"auth/popup-blocked\":\n return \"De browser blokkeerde het Google-venster. Sta popups toe voor deze site, of gebruik de inloglink hieronder.\";\n case \"auth/popup-closed-by-user\":\n case \"auth/cancelled-popup-request\":\n return \"Het Google-venster werd gesloten voordat het inloggen klaar was. Probeer het opnieuw, of gebruik de inloglink hieronder.\";\n // ── Inloglink ──────────────────────────────────────────────────────────────────────────\n case \"auth/expired-action-code\":\n return \"Deze inloglink is verlopen. Vraag hieronder een nieuwe aan.\";\n case \"auth/invalid-action-code\":\n return \"Deze inloglink is al gebruikt of niet meer geldig. Vraag hieronder een nieuwe aan.\";\n default:\n return code ? `${fallback} (${code})` : fallback;\n }\n}\n\n/**\n * Inlogscherm voor beheerders/editors — wachtwoordloos van opzet.\n *\n * Twee wegen naar binnen: Google, of een inloglink per mail. Geen van beide kent een wachtwoord,\n * en dus ook geen vergeten-wachtwoord-mailtjes. De inloglink is bovendien de betrouwbaarste van de\n * twee: hij raakt geen enkel domein van een derde partij en werkt daarom ook in browsers die\n * opslag van derden blokkeren — precies waar de Google-redirect op stukliep.\n *\n * Het wachtwoordformulier is niet weggegooid maar weggevouwen. Als beide wachtwoordloze wegen over\n * e-mail lopen, is e-mail een enkelvoudig storingspunt: hapert de mailbezorging, dan komt niemand\n * er meer in — ook de beheerder niet. Deze terugval blijft daarom bestaan voor wie hem nodig heeft,\n * zonder hem aan iedereen op te dringen.\n */\nexport function LoginForm({ onSuccess }: { onSuccess?: () => void }) {\n const {\n signIn,\n signInWithGoogle,\n sendLoginLink,\n loginLinkNeedsEmail,\n loginLinkError,\n completeLoginLink,\n configured,\n user,\n } = useAuth();\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n const [linkVerstuurd, setLinkVerstuurd] = useState(false);\n const [wachtwoordModus, setWachtwoordModus] = useState(false);\n const [busy, setBusy] = useState(false);\n\n // Een mislukte link-inwisseling gebeurt in de provider (bij het laden), niet hier. Zonder deze\n // doorgifte zou de gebruiker een leeg inlogscherm zien terwijl er wél iets mis was.\n useEffect(() => {\n if (loginLinkError) setError(authErrorMessage(loginLinkError, \"Inloggen via de link mislukt.\"));\n }, [loginLinkError]);\n\n async function handleGoogle() {\n setError(null);\n setBusy(true);\n try {\n await signInWithGoogle();\n onSuccess?.();\n } catch (e) {\n setError(authErrorMessage(e, \"Inloggen met Google mislukt of geannuleerd.\"));\n } finally {\n setBusy(false);\n }\n }\n\n if (!configured) {\n return (\n <div className=\"xicms-card\" style={{ padding: \"1.5rem\" }}>\n <p>\n Firebase is nog niet geconfigureerd. Vul de <code>NEXT_PUBLIC_FIREBASE_*</code>\n omgevingsvariabelen in om inloggen aan te zetten.\n </p>\n </div>\n );\n }\n\n if (user) {\n return (\n <div className=\"xicms-card\" style={{ padding: \"1.5rem\" }}>\n <p>Je bent ingelogd als <strong>{user.email}</strong>.</p>\n </div>\n );\n }\n\n /** Verstuur de inloglink, of — bij terugkomst zonder bekend adres — wissel de link juist in. */\n async function handleEmail(e: FormEvent) {\n e.preventDefault();\n setError(null);\n setBusy(true);\n try {\n if (loginLinkNeedsEmail) {\n await completeLoginLink(email);\n onSuccess?.();\n } else {\n await sendLoginLink(email);\n setLinkVerstuurd(true);\n }\n } catch (err) {\n setError(authErrorMessage(err, \"Versturen van de inloglink mislukt.\"));\n } finally {\n setBusy(false);\n }\n }\n\n async function handleWachtwoord(e: FormEvent) {\n e.preventDefault();\n setError(null);\n setBusy(true);\n try {\n await signIn(email, password);\n onSuccess?.();\n } catch (err) {\n setError(authErrorMessage(err, \"Inloggen mislukt. Controleer je e-mailadres en wachtwoord.\"));\n } finally {\n setBusy(false);\n }\n }\n\n const kaart: CSSProperties = {\n padding: \"1.5rem\",\n display: \"grid\",\n gap: \"0.75rem\",\n maxWidth: 360,\n };\n\n // Bevestiging ná het versturen. Bewust geen formulier meer: de volgende stap staat in de mailbox,\n // en een nog zichtbaar veld nodigt uit tot nog een keer klikken (waarmee de eerste link vervalt).\n if (linkVerstuurd) {\n return (\n <div className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Kijk in je mail</h2>\n <p style={{ margin: 0 }}>\n We stuurden een inloglink naar <strong>{email}</strong>. Klik erop en je bent binnen — geen\n wachtwoord nodig.\n </p>\n <p style={{ margin: 0, fontSize: 13, opacity: 0.7 }}>\n Niets ontvangen? Kijk in je spam, of{\" \"}\n <button\n type=\"button\"\n onClick={() => setLinkVerstuurd(false)}\n className=\"xicms-btn xicms-btn--ghost\"\n style={{ padding: 0, border: 0, background: \"none\", textDecoration: \"underline\", cursor: \"pointer\" }}\n >\n probeer een ander adres\n </button>\n .\n </p>\n </div>\n );\n }\n\n if (wachtwoordModus) {\n return (\n <form onSubmit={handleWachtwoord} className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Inloggen met wachtwoord</h2>\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>E-mailadres</span>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"xicms-input\"\n />\n </label>\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>Wachtwoord</span>\n <input\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n autoComplete=\"current-password\"\n className=\"xicms-input\"\n />\n </label>\n {error && <p style={{ color: \"#dc2626\", margin: 0 }}>{error}</p>}\n <button type=\"submit\" disabled={busy} className=\"xicms-btn\">\n {busy ? \"Bezig…\" : \"Inloggen\"}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setWachtwoordModus(false);\n setError(null);\n }}\n className=\"xicms-btn xicms-btn--ghost\"\n >\n Liever zonder wachtwoord\n </button>\n </form>\n );\n }\n\n return (\n <form onSubmit={handleEmail} className=\"xicms-card\" style={kaart}>\n <h2 style={{ margin: 0 }}>Inloggen</h2>\n\n {loginLinkNeedsEmail && (\n <p style={{ margin: 0, fontSize: 14 }}>\n Bevestig even bij welk e-mailadres deze inloglink hoort. Dat is wat de link beveiligt: wie\n hem onderschept maar het adres niet weet, komt er niet in.\n </p>\n )}\n\n <button type=\"button\" onClick={handleGoogle} disabled={busy} className=\"xicms-btn\">\n Inloggen met Google\n </button>\n\n <div style={{ display: \"flex\", alignItems: \"center\", gap: \"0.5rem\", opacity: 0.6, fontSize: 13 }}>\n <span style={{ flex: 1, height: 1, background: \"currentColor\" }} /> of{\" \"}\n <span style={{ flex: 1, height: 1, background: \"currentColor\" }} />\n </div>\n\n <label style={{ display: \"grid\", gap: \"0.25rem\" }}>\n <span>E-mailadres</span>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"xicms-input\"\n />\n </label>\n {error && <p style={{ color: \"#dc2626\", margin: 0 }}>{error}</p>}\n <button type=\"submit\" disabled={busy} className=\"xicms-btn xicms-btn--ghost\">\n {busy ? \"Bezig…\" : loginLinkNeedsEmail ? \"Inloggen afronden\" : \"Stuur mij een inloglink\"}\n </button>\n\n {/* De terugval: aanwezig voor wie hem nodig heeft, stil voor wie dat niet heeft. */}\n {!loginLinkNeedsEmail && (\n <button\n type=\"button\"\n onClick={() => {\n setWachtwoordModus(true);\n setError(null);\n }}\n style={{\n justifySelf: \"start\",\n padding: 0,\n border: 0,\n background: \"none\",\n font: \"inherit\",\n fontSize: 12,\n opacity: 0.55,\n textDecoration: \"underline\",\n cursor: \"pointer\",\n }}\n >\n Inloggen met wachtwoord\n </button>\n )}\n </form>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toont een externe pagina in een kader BINNEN de site, onder de logo/menubalk.
|
|
3
|
+
*
|
|
4
|
+
* De klassen heten `xicms-extern*` en niet `xicms-embed*`: die laatste naam is al in gebruik voor
|
|
5
|
+
* het consent-blok dat video's en kaarten inkadert. Twee ongerelateerde dingen met dezelfde
|
|
6
|
+
* klassenaam gaan elkaars opmaak overschrijven, en dat merk je pas als er ergens iets scheef staat.
|
|
7
|
+
*
|
|
8
|
+
* TWEE DINGEN DIE HIER MISGAAN ALS JE ZE NIET REGELT.
|
|
9
|
+
*
|
|
10
|
+
* 1. WIE BEPAALT WAT ER IN HET KADER KOMT. De URL staat in de adresbalk, dus iedereen kan hem
|
|
11
|
+
* aanpassen. Zonder controle kan een willekeurige vreemde pagina in jouw huisstijl verschijnen,
|
|
12
|
+
* compleet met jouw logo eromheen — een uitstekend vertrekpunt voor phishing. Daarom laden we
|
|
13
|
+
* alleen adressen die de sitebeheerder zélf als ingekaderd menu-item heeft ingesteld.
|
|
14
|
+
*
|
|
15
|
+
* 2. HET KADER BLIJFT VAAK LEEG. Veel sites weigeren te worden ingekaderd (`X-Frame-Options` of
|
|
16
|
+
* `frame-ancestors`). Dat kunnen we niet omzeilen — het is hun keuze — maar we kunnen wél
|
|
17
|
+
* voorkomen dat de bezoeker naar een wit vlak zit te kijken zonder te weten waarom. Laadt er
|
|
18
|
+
* binnen een paar tellen niets, dan tonen we een uitleg met een gewone link.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ExternEmbed({ url, hoogte }: {
|
|
21
|
+
url: string;
|
|
22
|
+
hoogte?: string;
|
|
23
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { useMergedSiteConfig } from "../settings/useSiteSettings.js";
|
|
5
|
+
import { isToegestaneEmbed } from "../schema/settings.js";
|
|
6
|
+
function ExternEmbed({ url, hoogte = "70vh" }) {
|
|
7
|
+
const cfg = useMergedSiteConfig();
|
|
8
|
+
const [geladen, setGeladen] = useState(false);
|
|
9
|
+
const [traag, setTraag] = useState(false);
|
|
10
|
+
const frame = useRef(null);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const t = setTimeout(() => setTraag(true), 4e3);
|
|
13
|
+
return () => clearTimeout(t);
|
|
14
|
+
}, [url]);
|
|
15
|
+
if (!isToegestaneEmbed(url, cfg.nav)) {
|
|
16
|
+
return /* @__PURE__ */ jsxs("div", { className: "xicms-extern__melding", children: [
|
|
17
|
+
/* @__PURE__ */ jsx("h1", { children: "Pagina niet beschikbaar" }),
|
|
18
|
+
/* @__PURE__ */ jsx("p", { children: "Deze pagina hoort niet bij het menu van deze site en wordt daarom niet getoond." })
|
|
19
|
+
] });
|
|
20
|
+
}
|
|
21
|
+
let host = url;
|
|
22
|
+
try {
|
|
23
|
+
host = new URL(url).host;
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
return /* @__PURE__ */ jsxs("div", { className: "xicms-extern", children: [
|
|
27
|
+
/* @__PURE__ */ jsx(
|
|
28
|
+
"iframe",
|
|
29
|
+
{
|
|
30
|
+
ref: frame,
|
|
31
|
+
src: url,
|
|
32
|
+
title: host,
|
|
33
|
+
className: "xicms-extern__frame",
|
|
34
|
+
style: { height: hoogte },
|
|
35
|
+
onLoad: () => setGeladen(true),
|
|
36
|
+
sandbox: "allow-scripts allow-forms allow-same-origin",
|
|
37
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
38
|
+
loading: "lazy"
|
|
39
|
+
}
|
|
40
|
+
),
|
|
41
|
+
traag && !geladen && /* @__PURE__ */ jsxs("p", { className: "xicms-extern__uitleg", children: [
|
|
42
|
+
/* @__PURE__ */ jsx("strong", { children: host }),
|
|
43
|
+
" laat zich niet in een kader tonen. Veel sites staan dat niet toe.",
|
|
44
|
+
" ",
|
|
45
|
+
/* @__PURE__ */ jsx("a", { href: url, target: "_blank", rel: "noopener noreferrer", children: "Open de pagina in een nieuw venster \u2197" })
|
|
46
|
+
] })
|
|
47
|
+
] });
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
ExternEmbed
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=ExternEmbed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/ExternEmbed.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { useMergedSiteConfig } from \"../settings/useSiteSettings\";\nimport { isToegestaneEmbed } from \"../schema/settings\";\n\n/**\n * Toont een externe pagina in een kader BINNEN de site, onder de logo/menubalk.\n *\n * De klassen heten `xicms-extern*` en niet `xicms-embed*`: die laatste naam is al in gebruik voor\n * het consent-blok dat video's en kaarten inkadert. Twee ongerelateerde dingen met dezelfde\n * klassenaam gaan elkaars opmaak overschrijven, en dat merk je pas als er ergens iets scheef staat.\n *\n * TWEE DINGEN DIE HIER MISGAAN ALS JE ZE NIET REGELT.\n *\n * 1. WIE BEPAALT WAT ER IN HET KADER KOMT. De URL staat in de adresbalk, dus iedereen kan hem\n * aanpassen. Zonder controle kan een willekeurige vreemde pagina in jouw huisstijl verschijnen,\n * compleet met jouw logo eromheen — een uitstekend vertrekpunt voor phishing. Daarom laden we\n * alleen adressen die de sitebeheerder zélf als ingekaderd menu-item heeft ingesteld.\n *\n * 2. HET KADER BLIJFT VAAK LEEG. Veel sites weigeren te worden ingekaderd (`X-Frame-Options` of\n * `frame-ancestors`). Dat kunnen we niet omzeilen — het is hun keuze — maar we kunnen wél\n * voorkomen dat de bezoeker naar een wit vlak zit te kijken zonder te weten waarom. Laadt er\n * binnen een paar tellen niets, dan tonen we een uitleg met een gewone link.\n */\nexport function ExternEmbed({ url, hoogte = \"70vh\" }: { url: string; hoogte?: string }) {\n const cfg = useMergedSiteConfig();\n const [geladen, setGeladen] = useState(false);\n const [traag, setTraag] = useState(false);\n const frame = useRef<HTMLIFrameElement>(null);\n\n // `load` vuurt óók als de externe site het inkaderen weigert — de browser laadt dan een lege\n // pagina en meldt dat als succes. Vandaar niet alléén op `load` vertrouwen maar op een klok: is\n // er na een paar tellen niets zichtbaars, dan is er iets aan de hand en zeggen we dat.\n useEffect(() => {\n const t = setTimeout(() => setTraag(true), 4000);\n return () => clearTimeout(t);\n }, [url]);\n\n if (!isToegestaneEmbed(url, cfg.nav)) {\n return (\n <div className=\"xicms-extern__melding\">\n <h1>Pagina niet beschikbaar</h1>\n <p>Deze pagina hoort niet bij het menu van deze site en wordt daarom niet getoond.</p>\n </div>\n );\n }\n\n let host = url;\n try {\n host = new URL(url).host;\n } catch {\n /* onmogelijk na de controle hierboven, maar een kapotte URL mag nooit de pagina slopen */\n }\n\n return (\n <div className=\"xicms-extern\">\n <iframe\n ref={frame}\n src={url}\n title={host}\n className=\"xicms-extern__frame\"\n style={{ height: hoogte }}\n onLoad={() => setGeladen(true)}\n // Zo krap als het kan terwijl een normale pagina blijft werken: geen toegang tot onze\n // opslag, geen popups, en niet zelf de bovenliggende pagina kunnen wegnavigeren.\n sandbox=\"allow-scripts allow-forms allow-same-origin\"\n referrerPolicy=\"strict-origin-when-cross-origin\"\n loading=\"lazy\"\n />\n {traag && !geladen && (\n <p className=\"xicms-extern__uitleg\">\n <strong>{host}</strong> laat zich niet in een kader tonen. Veel sites staan dat niet toe.{\" \"}\n <a href={url} target=\"_blank\" rel=\"noopener noreferrer\">Open de pagina in een nieuw venster ↗</a>\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";AAyCM,SACE,KADF;AAvCN,SAAS,WAAW,QAAQ,gBAAgB;AAC5C,SAAS,2BAA2B;AACpC,SAAS,yBAAyB;AAqB3B,SAAS,YAAY,EAAE,KAAK,SAAS,OAAO,GAAqC;AACtF,QAAM,MAAM,oBAAoB;AAChC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK;AACxC,QAAM,QAAQ,OAA0B,IAAI;AAK5C,YAAU,MAAM;AACd,UAAM,IAAI,WAAW,MAAM,SAAS,IAAI,GAAG,GAAI;AAC/C,WAAO,MAAM,aAAa,CAAC;AAAA,EAC7B,GAAG,CAAC,GAAG,CAAC;AAER,MAAI,CAAC,kBAAkB,KAAK,IAAI,GAAG,GAAG;AACpC,WACE,qBAAC,SAAI,WAAU,yBACb;AAAA,0BAAC,QAAG,qCAAuB;AAAA,MAC3B,oBAAC,OAAE,6FAA+E;AAAA,OACpF;AAAA,EAEJ;AAEA,MAAI,OAAO;AACX,MAAI;AACF,WAAO,IAAI,IAAI,GAAG,EAAE;AAAA,EACtB,QAAQ;AAAA,EAER;AAEA,SACE,qBAAC,SAAI,WAAU,gBACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,KAAK;AAAA,QACL,OAAO;AAAA,QACP,WAAU;AAAA,QACV,OAAO,EAAE,QAAQ,OAAO;AAAA,QACxB,QAAQ,MAAM,WAAW,IAAI;AAAA,QAG7B,SAAQ;AAAA,QACR,gBAAe;AAAA,QACf,SAAQ;AAAA;AAAA,IACV;AAAA,IACC,SAAS,CAAC,WACT,qBAAC,OAAE,WAAU,wBACX;AAAA,0BAAC,YAAQ,gBAAK;AAAA,MAAS;AAAA,MAAmE;AAAA,MAC1F,oBAAC,OAAE,MAAM,KAAK,QAAO,UAAS,KAAI,uBAAsB,wDAAqC;AAAA,OAC/F;AAAA,KAEJ;AAEJ;","names":[]}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
+
import type { Page } from "../schema/pages.js";
|
|
3
|
+
type PaginaBlokken = Page["blocks"];
|
|
2
4
|
interface EditorCtx {
|
|
3
5
|
open: boolean;
|
|
4
6
|
openEditor: () => void;
|
|
5
7
|
closeEditor: () => void;
|
|
8
|
+
/** De blokken van de werkversie die de pagina moet tonen; `null` = toon gewoon het gepubliceerde. */
|
|
9
|
+
draftBlocks: PaginaBlokken | null;
|
|
10
|
+
/** Het pad waarvoor `draftBlocks` geldt. */
|
|
11
|
+
editPath: string | null;
|
|
12
|
+
/** Door de editor aangeroepen zodra de werkversie laadt of wijzigt. */
|
|
13
|
+
publishDraft: (path: string, blocks: PaginaBlokken | null) => void;
|
|
6
14
|
}
|
|
7
15
|
export declare function EditorProvider({ children }: {
|
|
8
16
|
children: ReactNode;
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useContext, useState, lazy, Suspense } from "react";
|
|
3
|
+
import { createContext, useCallback, useContext, useMemo, useState, lazy, Suspense } from "react";
|
|
4
4
|
const VisualEditor = lazy(() => import("../admin/visual/VisualEditor.js"));
|
|
5
|
-
const Ctx = createContext({
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const Ctx = createContext({
|
|
6
|
+
open: false,
|
|
7
|
+
openEditor: () => {
|
|
8
|
+
},
|
|
9
|
+
closeEditor: () => {
|
|
10
|
+
},
|
|
11
|
+
draftBlocks: null,
|
|
12
|
+
editPath: null,
|
|
13
|
+
publishDraft: () => {
|
|
14
|
+
}
|
|
15
|
+
});
|
|
8
16
|
function EditorProvider({ children }) {
|
|
9
17
|
const [open, setOpen] = useState(false);
|
|
10
|
-
|
|
18
|
+
const [draft, setDraft] = useState(null);
|
|
19
|
+
const publishDraft = useCallback((path, blocks) => {
|
|
20
|
+
setDraft(blocks ? { path, blocks } : null);
|
|
21
|
+
}, []);
|
|
22
|
+
const closeEditor = useCallback(() => {
|
|
23
|
+
setOpen(false);
|
|
24
|
+
setDraft(null);
|
|
25
|
+
}, []);
|
|
26
|
+
const value = useMemo(() => ({
|
|
27
|
+
open,
|
|
28
|
+
openEditor: () => setOpen(true),
|
|
29
|
+
closeEditor,
|
|
30
|
+
draftBlocks: draft?.blocks ?? null,
|
|
31
|
+
editPath: draft?.path ?? null,
|
|
32
|
+
publishDraft
|
|
33
|
+
}), [open, draft, closeEditor, publishDraft]);
|
|
34
|
+
return /* @__PURE__ */ jsxs(Ctx.Provider, { value, children: [
|
|
11
35
|
children,
|
|
12
|
-
open && /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(VisualEditor, { onClose:
|
|
36
|
+
open && /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(VisualEditor, { onClose: closeEditor }) })
|
|
13
37
|
] });
|
|
14
38
|
}
|
|
15
39
|
function useEditor() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/editor/EditorProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useState, lazy, Suspense, type ReactNode } from \"react\";\n\n/**\n * Lichte runtime-context voor het inline (visuele) bewerken op de live pagina. De echte editor\n * (TipTap/Radix, zwaar) wordt PAS geladen als een editor hem opent — publieke bezoekers downloaden\n * hem nooit. De editor leeft in de admin-entry; we laden 'm via een dynamische import (eigen chunk).\n */\nconst VisualEditor = lazy(() => import(\"../admin/visual/VisualEditor\"));\n\ninterface EditorCtx {\n open: boolean;\n openEditor: () => void;\n closeEditor: () => void;\n}\n\nconst Ctx = createContext<EditorCtx>({
|
|
1
|
+
{"version":3,"sources":["../../src/editor/EditorProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useCallback, useContext, useMemo, useState, lazy, Suspense, type ReactNode } from \"react\";\n// Het bloktype van de PAGINA zelf, zodat wat hier langskomt per definitie past in wat\n// `PublishedPage` rendert. Een eigen definitie zou hier stil uit de pas kunnen lopen.\nimport type { Page } from \"../schema/pages\";\n\ntype PaginaBlokken = Page[\"blocks\"];\n\n/**\n * Lichte runtime-context voor het inline (visuele) bewerken op de live pagina. De echte editor\n * (TipTap/Radix, zwaar) wordt PAS geladen als een editor hem opent — publieke bezoekers downloaden\n * hem nooit. De editor leeft in de admin-entry; we laden 'm via een dynamische import (eigen chunk).\n *\n * ── WAAROM DE BLOKKEN HIER LANGSKOMEN ─────────────────────────────────────────────────────\n *\n * De editor tekende de pagina vroeger zélf, in een eigen canvas binnen een schermvullende overlay.\n * Je klikte \"Pagina bewerken\" en je site verdween achter een donker paneel met een nabootsing erin.\n * Dat voelde niet als je site bewerken maar als een ander programma openen.\n *\n * Nu blijft de PAGINA de pagina en wordt alleen de DATA vervangen: de editor laadt de werkversie en\n * zet die hier neer, waarna `PublishedPage` de werkversie rendert in plaats van de gepubliceerde\n * blokken. Dezelfde componenten, dezelfde huisstijl, dezelfde layout — alleen andere inhoud.\n *\n * Die omweg via de data is wezenlijk, geen implementatiedetail. Alleen potloodjes over de live\n * pagina leggen zou betekenen dat je de WERKVERSIE bewerkt terwijl je de GEPUBLICEERDE tekst ziet.\n * Zodra je iets wijzigt en nog niet publiceert, lopen die twee uit elkaar en bewerk je iets anders\n * dan er staat. Wat je ziet moet zijn wat je bewerkt.\n *\n * `editPath` hoort erbij: een site kan meerdere gerenderde pagina's in beeld hebben (een\n * ingekaderde pagina, een preview). Alleen het pad dat de editor open heeft mag zijn blokken\n * verwisselen.\n */\nconst VisualEditor = lazy(() => import(\"../admin/visual/VisualEditor\"));\n\ninterface EditorCtx {\n open: boolean;\n openEditor: () => void;\n closeEditor: () => void;\n /** De blokken van de werkversie die de pagina moet tonen; `null` = toon gewoon het gepubliceerde. */\n draftBlocks: PaginaBlokken | null;\n /** Het pad waarvoor `draftBlocks` geldt. */\n editPath: string | null;\n /** Door de editor aangeroepen zodra de werkversie laadt of wijzigt. */\n publishDraft: (path: string, blocks: PaginaBlokken | null) => void;\n}\n\nconst Ctx = createContext<EditorCtx>({\n open: false,\n openEditor: () => {},\n closeEditor: () => {},\n draftBlocks: null,\n editPath: null,\n publishDraft: () => {},\n});\n\nexport function EditorProvider({ children }: { children: ReactNode }) {\n const [open, setOpen] = useState(false);\n const [draft, setDraft] = useState<{ path: string; blocks: PaginaBlokken } | null>(null);\n\n const publishDraft = useCallback((path: string, blocks: PaginaBlokken | null) => {\n setDraft(blocks ? { path, blocks } : null);\n }, []);\n\n const closeEditor = useCallback(() => {\n setOpen(false);\n // De werkversie mee opruimen: laat je hem staan, dan blijft de bezoeker na het sluiten de\n // ongepubliceerde tekst zien terwijl de balk weg is — een pagina die iets toont wat niet live is.\n setDraft(null);\n }, []);\n\n const value = useMemo<EditorCtx>(() => ({\n open,\n openEditor: () => setOpen(true),\n closeEditor,\n draftBlocks: draft?.blocks ?? null,\n editPath: draft?.path ?? null,\n publishDraft,\n }), [open, draft, closeEditor, publishDraft]);\n\n return (\n <Ctx.Provider value={value}>\n {children}\n {open && (\n <Suspense fallback={null}>\n <VisualEditor onClose={closeEditor} />\n </Suspense>\n )}\n </Ctx.Provider>\n );\n}\n\n/** Open/sluit de visuele pagina-editor. */\nexport function useEditor(): EditorCtx {\n return useContext(Ctx);\n}\n"],"mappings":";AAiFI,SAIM,KAJN;AA/EJ,SAAS,eAAe,aAAa,YAAY,SAAS,UAAU,MAAM,gBAAgC;AA+B1G,MAAM,eAAe,KAAK,MAAM,OAAO,8BAA8B,CAAC;AActE,MAAM,MAAM,cAAyB;AAAA,EACnC,MAAM;AAAA,EACN,YAAY,MAAM;AAAA,EAAC;AAAA,EACnB,aAAa,MAAM;AAAA,EAAC;AAAA,EACpB,aAAa;AAAA,EACb,UAAU;AAAA,EACV,cAAc,MAAM;AAAA,EAAC;AACvB,CAAC;AAEM,SAAS,eAAe,EAAE,SAAS,GAA4B;AACpE,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAyD,IAAI;AAEvF,QAAM,eAAe,YAAY,CAAC,MAAc,WAAiC;AAC/E,aAAS,SAAS,EAAE,MAAM,OAAO,IAAI,IAAI;AAAA,EAC3C,GAAG,CAAC,CAAC;AAEL,QAAM,cAAc,YAAY,MAAM;AACpC,YAAQ,KAAK;AAGb,aAAS,IAAI;AAAA,EACf,GAAG,CAAC,CAAC;AAEL,QAAM,QAAQ,QAAmB,OAAO;AAAA,IACtC;AAAA,IACA,YAAY,MAAM,QAAQ,IAAI;AAAA,IAC9B;AAAA,IACA,aAAa,OAAO,UAAU;AAAA,IAC9B,UAAU,OAAO,QAAQ;AAAA,IACzB;AAAA,EACF,IAAI,CAAC,MAAM,OAAO,aAAa,YAAY,CAAC;AAE5C,SACE,qBAAC,IAAI,UAAJ,EAAa,OACX;AAAA;AAAA,IACA,QACC,oBAAC,YAAS,UAAU,MAClB,8BAAC,gBAAa,SAAS,aAAa,GACtC;AAAA,KAEJ;AAEJ;AAGO,SAAS,YAAuB;AACrC,SAAO,WAAW,GAAG;AACvB;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export { LoginForm } from "./auth/LoginForm.js";
|
|
|
39
39
|
export { roleForSite, canEdit, canManageSite, isSuperadmin, type Role, type XiClaims, } from "./auth/roles.js";
|
|
40
40
|
export type { NewsArticle, MediaItem, MediaVariant } from "./content/types.js";
|
|
41
41
|
export { EditBar } from "./components/EditBar.js";
|
|
42
|
+
export { ExternEmbed } from "./components/ExternEmbed.js";
|
|
42
43
|
export { ResponsiveImage } from "./components/ResponsiveImage.js";
|
|
43
44
|
export { PageView } from "./components/PageView.js";
|
|
44
45
|
export { NewsList, NewsArticleView, useNews } from "./components/News.js";
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
isSuperadmin
|
|
43
43
|
} from "./auth/roles.js";
|
|
44
44
|
import { EditBar } from "./components/EditBar.js";
|
|
45
|
+
import { ExternEmbed } from "./components/ExternEmbed.js";
|
|
45
46
|
import { ResponsiveImage } from "./components/ResponsiveImage.js";
|
|
46
47
|
import { PageView } from "./components/PageView.js";
|
|
47
48
|
import { NewsList, NewsArticleView, useNews } from "./components/News.js";
|
|
@@ -100,6 +101,7 @@ export {
|
|
|
100
101
|
DemoLock,
|
|
101
102
|
EditBar,
|
|
102
103
|
EditorProvider,
|
|
104
|
+
ExternEmbed,
|
|
103
105
|
FieldSchema,
|
|
104
106
|
FieldsSchema,
|
|
105
107
|
ImageProvider,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Publieke runtime-API van @xiwebdesign/xicms-core (lichte client/runtime laag).\n * - Admin (TipTap/Radix) zit in \"@xiwebdesign/xicms-core/admin\".\n * - Server-only code (firebase-admin, sanitize, SEO) zit in \"@xiwebdesign/xicms-core/server\".\n * - Pure schema's in \"@xiwebdesign/xicms-core/schema\".\n */\n\n// Provider & config\nexport { XiCMSProvider } from \"./XiCMSProvider\";\nexport { useSiteConfig } from \"./config\";\n\n// Theming & design-primitieven (maatwerk zonder core te forken).\n// - `defineTheme` maakt een eigen Astryx-thema (tokens + component-overrides) → geef door als\n// XiCMSProvider `astryxTheme`-prop.\n// - De `ui`-primitieven (Card/Stack/Grid/Heading/Text/Btn…) zijn de bouwstenen waarop de ingebouwde\n// blokken draaien; gebruik ze in eigen blokken voor een consistent maar te herstijlen uiterlijk.\nexport { defineTheme, useTheme } from \"@astryxdesign/core/theme\";\nexport type { DefinedTheme } from \"@astryxdesign/core/theme\";\n// Framework-router-integratie: geef `next/link` door als XiCMSProvider `linkComponent`-prop\n// (soepele client-nav + prefetch op alle Astryx-knoppen/links). `useLinkComponent` voor eigen blokken.\nexport { LinkProvider, useLinkComponent } from \"@astryxdesign/core/Link\";\nexport type { LinkComponentType } from \"@astryxdesign/core/Link\";\n// Beeldoptimalisatie: geef `next/image` door als XiCMSProvider `imageComponent`-prop. `Img` is de\n// wrapper die de blokken gebruiken; `useImageComponent` voor eigen blokken.\nexport { Img, ImageProvider, useImageComponent, InlineImageEditProvider } from \"./media/ImageContext\";\nexport type { ImageComponentType, ImgProps } from \"./media/ImageContext\";\nexport * from \"./blocks/ui\";\nexport {\n SiteSettingsProvider,\n useSiteSettings,\n useMergedSiteConfig,\n} from \"./settings/useSiteSettings\";\nexport { SiteFooter } from \"./components/site/SiteFooter\";\nexport { EditorProvider, useEditor } from \"./editor/EditorProvider\";\nexport {\n definePattern,\n instantiatePattern,\n collectPatternTypes,\n builtinPatterns,\n type Pattern,\n type PatternBlock,\n} from \"./patterns\";\nexport { builtinThemePresets, type ThemePreset } from \"./theme/presets\";\nexport { defineSiteConfig } from \"./schema/siteConfig\";\nexport type { SiteConfig, SiteMode } from \"./schema/siteConfig\";\n// 5.0: Field-IR (primo-port), symbolen en custom blocks.\nexport { FieldSchema, FieldsSchema, fieldsToZod, fieldsDefaults, isFieldVisible, isRichTextKey } from \"./schema/fields\";\nexport type { Field, FieldHints, ShowIf } from \"./schema/fields\";\nexport { SymbolSchema } from \"./schema/symbols\";\nexport type { SymbolDefinition, StoredSymbol } from \"./schema/symbols\";\nexport { CustomBlockSchema } from \"./schema/customBlocks\";\nexport type { CustomBlock, CustomBlock as CustomBlockData } from \"./schema/customBlocks\";\nexport { customBlockToDefinition } from \"./blocks/customTemplate/definition\";\nexport { interpretTemplate, scopeCss } from \"./blocks/customTemplate/interpret\";\n// 5.0: localisatie — overlay-model (primo's locale-per-waarde, additief vertaald).\nexport {\n siteLocales,\n parseLocaleSlugs,\n localizedPath,\n localizePage,\n localizeBlocks,\n pruneTranslations,\n TranslationsSchema,\n} from \"./i18n/overlay\";\nexport type { Translations, LocaleConfig } from \"./i18n/overlay\";\nexport type { ThemeTokens, NavItem, SiteSettings, PublicSiteSettings, FormsSender } from \"./schema/settings\";\nexport type { Seo } from \"./schema/seo\";\nexport type { Page, PageStatus } from \"./schema/pages\";\nexport type { FormDefinition, FormField } from \"./schema/forms\";\n\n// Auth\nexport { AuthProvider, useAuth } from \"./auth/AuthProvider\";\nexport { LoginForm } from \"./auth/LoginForm\";\nexport {\n roleForSite,\n canEdit,\n canManageSite,\n isSuperadmin,\n type Role,\n type XiClaims,\n} from \"./auth/roles\";\n\n// De legacy inline-laag (ContentProvider/useContent + EditableText/EditableRichText/EditableImage,\n// het v1 platte `cms/content`-model) is in 5.0 VERWIJDERD, zoals bij de deprecatie aangekondigd.\n// Inline bewerken loopt via het block-model: markeer tekstvelden met `data-xicms-edit` in de Render.\nexport type { NewsArticle, MediaItem, MediaVariant } from \"./content/types\";\n\nexport { EditBar } from \"./components/EditBar\";\nexport { ResponsiveImage } from \"./components/ResponsiveImage\";\nexport { PageView } from \"./components/PageView\";\nexport { NewsList, NewsArticleView, useNews } from \"./components/News\";\n\n// Block-systeem (runtime: registry + renderer; editors zitten in /admin)\nexport {\n defineBlock,\n createRegistry,\n mergeRegistry,\n validateBlocks,\n buildBlockAddOptions,\n type BlockDefinition,\n type BlockRegistry,\n type BlockRenderProps,\n type BlockEditorProps,\n type BlockCategory,\n} from \"./blocks/registry\";\nexport { BlockRenderer } from \"./blocks/BlockRenderer\";\nexport { RegistryProvider, useRegistry, BlockDataProvider, useBlockData } from \"./blocks/context\";\nexport { builtinBlocks, ConsentEmbed } from \"./blocks/builtins\";\nexport { PageRenderer } from \"./render/PageRenderer\";\nexport { PublishedPage } from \"./render/PublishedPage\";\n\n// Module-systeem (XiCMS 3.0): modules bundelen blocks + instellingen; per site aan/uit + te stylen.\nexport {\n defineModule,\n resolveModules,\n isModuleActive,\n ModulesProvider,\n useModules,\n useModuleActive,\n useModuleSettings,\n applyBlockOverrides,\n moduleIdForBlockType,\n builtinModules,\n validateModuleContract,\n assertModuleContract,\n useConsent,\n ConsentBanner,\n ConsentGate,\n ConsentReopenLink,\n type ConsentState,\n type ConsentSettings,\n type ConsentCategory,\n type ModuleDefinition,\n type ModuleSiteSetting,\n type ModulesSiteConfig,\n type Capability,\n type ResolvedModules,\n type BlockOverride,\n type BlockOverrides,\n} from \"./modules\";\n\n// Demo-lock\nexport { DemoLock } from \"./lock/DemoLock\";\n\n// Demo-bewerkmodus (lokaal-only) leeft op het subpad `@xiwebdesign/xicms-core/demo` — dit zijn\n// demo-store-internals die niet in de publieke runtime-API thuishoren (klantsites gebruiken ze nooit;\n// alleen de XiCMS.nl-showcase). DemoStudio wordt intern door XiCMSProvider gerenderd.\n\n// Firebase (client)\nexport {\n getFirebaseApp,\n getFirebaseAuth,\n getDb,\n getBucket,\n isFirebaseConfigured,\n} from \"./firebase/client\";\nexport { initAppCheck } from \"./firebase/appcheck\";\nexport { SiteChrome } from \"./SiteChrome\";\nexport { CORE_VERSION, CORE_PACKAGE, isNieuwer } from \"./version\";\n"],"mappings":"AAQA,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAO9B,SAAS,aAAa,gBAAgB;AAItC,SAAS,cAAc,wBAAwB;AAI/C,SAAS,KAAK,eAAe,mBAAmB,+BAA+B;AAE/E,cAAc;AACd;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB,iBAAiB;AAC1C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,SAAS,2BAA6C;AACtD,SAAS,wBAAwB;AAGjC,SAAS,aAAa,cAAc,aAAa,gBAAgB,gBAAgB,qBAAqB;AAEtG,SAAS,oBAAoB;AAE7B,SAAS,yBAAyB;AAElC,SAAS,+BAA+B;AACxC,SAAS,mBAAmB,gBAAgB;AAE5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQP,SAAS,cAAc,eAAe;AACtC,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAOP,SAAS,eAAe;AACxB,SAAS,uBAAuB;AAChC,SAAS,gBAAgB;AACzB,SAAS,UAAU,iBAAiB,eAAe;AAGnD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAMK;AACP,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB,aAAa,mBAAmB,oBAAoB;AAC/E,SAAS,eAAe,oBAAoB;AAC5C,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAG9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAWK;AAGP,SAAS,gBAAgB;AAOzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,cAAc,cAAc,iBAAiB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Publieke runtime-API van @xiwebdesign/xicms-core (lichte client/runtime laag).\n * - Admin (TipTap/Radix) zit in \"@xiwebdesign/xicms-core/admin\".\n * - Server-only code (firebase-admin, sanitize, SEO) zit in \"@xiwebdesign/xicms-core/server\".\n * - Pure schema's in \"@xiwebdesign/xicms-core/schema\".\n */\n\n// Provider & config\nexport { XiCMSProvider } from \"./XiCMSProvider\";\nexport { useSiteConfig } from \"./config\";\n\n// Theming & design-primitieven (maatwerk zonder core te forken).\n// - `defineTheme` maakt een eigen Astryx-thema (tokens + component-overrides) → geef door als\n// XiCMSProvider `astryxTheme`-prop.\n// - De `ui`-primitieven (Card/Stack/Grid/Heading/Text/Btn…) zijn de bouwstenen waarop de ingebouwde\n// blokken draaien; gebruik ze in eigen blokken voor een consistent maar te herstijlen uiterlijk.\nexport { defineTheme, useTheme } from \"@astryxdesign/core/theme\";\nexport type { DefinedTheme } from \"@astryxdesign/core/theme\";\n// Framework-router-integratie: geef `next/link` door als XiCMSProvider `linkComponent`-prop\n// (soepele client-nav + prefetch op alle Astryx-knoppen/links). `useLinkComponent` voor eigen blokken.\nexport { LinkProvider, useLinkComponent } from \"@astryxdesign/core/Link\";\nexport type { LinkComponentType } from \"@astryxdesign/core/Link\";\n// Beeldoptimalisatie: geef `next/image` door als XiCMSProvider `imageComponent`-prop. `Img` is de\n// wrapper die de blokken gebruiken; `useImageComponent` voor eigen blokken.\nexport { Img, ImageProvider, useImageComponent, InlineImageEditProvider } from \"./media/ImageContext\";\nexport type { ImageComponentType, ImgProps } from \"./media/ImageContext\";\nexport * from \"./blocks/ui\";\nexport {\n SiteSettingsProvider,\n useSiteSettings,\n useMergedSiteConfig,\n} from \"./settings/useSiteSettings\";\nexport { SiteFooter } from \"./components/site/SiteFooter\";\nexport { EditorProvider, useEditor } from \"./editor/EditorProvider\";\nexport {\n definePattern,\n instantiatePattern,\n collectPatternTypes,\n builtinPatterns,\n type Pattern,\n type PatternBlock,\n} from \"./patterns\";\nexport { builtinThemePresets, type ThemePreset } from \"./theme/presets\";\nexport { defineSiteConfig } from \"./schema/siteConfig\";\nexport type { SiteConfig, SiteMode } from \"./schema/siteConfig\";\n// 5.0: Field-IR (primo-port), symbolen en custom blocks.\nexport { FieldSchema, FieldsSchema, fieldsToZod, fieldsDefaults, isFieldVisible, isRichTextKey } from \"./schema/fields\";\nexport type { Field, FieldHints, ShowIf } from \"./schema/fields\";\nexport { SymbolSchema } from \"./schema/symbols\";\nexport type { SymbolDefinition, StoredSymbol } from \"./schema/symbols\";\nexport { CustomBlockSchema } from \"./schema/customBlocks\";\nexport type { CustomBlock, CustomBlock as CustomBlockData } from \"./schema/customBlocks\";\nexport { customBlockToDefinition } from \"./blocks/customTemplate/definition\";\nexport { interpretTemplate, scopeCss } from \"./blocks/customTemplate/interpret\";\n// 5.0: localisatie — overlay-model (primo's locale-per-waarde, additief vertaald).\nexport {\n siteLocales,\n parseLocaleSlugs,\n localizedPath,\n localizePage,\n localizeBlocks,\n pruneTranslations,\n TranslationsSchema,\n} from \"./i18n/overlay\";\nexport type { Translations, LocaleConfig } from \"./i18n/overlay\";\nexport type { ThemeTokens, NavItem, SiteSettings, PublicSiteSettings, FormsSender } from \"./schema/settings\";\nexport type { Seo } from \"./schema/seo\";\nexport type { Page, PageStatus } from \"./schema/pages\";\nexport type { FormDefinition, FormField } from \"./schema/forms\";\n\n// Auth\nexport { AuthProvider, useAuth } from \"./auth/AuthProvider\";\nexport { LoginForm } from \"./auth/LoginForm\";\nexport {\n roleForSite,\n canEdit,\n canManageSite,\n isSuperadmin,\n type Role,\n type XiClaims,\n} from \"./auth/roles\";\n\n// De legacy inline-laag (ContentProvider/useContent + EditableText/EditableRichText/EditableImage,\n// het v1 platte `cms/content`-model) is in 5.0 VERWIJDERD, zoals bij de deprecatie aangekondigd.\n// Inline bewerken loopt via het block-model: markeer tekstvelden met `data-xicms-edit` in de Render.\nexport type { NewsArticle, MediaItem, MediaVariant } from \"./content/types\";\n\nexport { EditBar } from \"./components/EditBar\";\nexport { ExternEmbed } from \"./components/ExternEmbed\";\nexport { ResponsiveImage } from \"./components/ResponsiveImage\";\nexport { PageView } from \"./components/PageView\";\nexport { NewsList, NewsArticleView, useNews } from \"./components/News\";\n\n// Block-systeem (runtime: registry + renderer; editors zitten in /admin)\nexport {\n defineBlock,\n createRegistry,\n mergeRegistry,\n validateBlocks,\n buildBlockAddOptions,\n type BlockDefinition,\n type BlockRegistry,\n type BlockRenderProps,\n type BlockEditorProps,\n type BlockCategory,\n} from \"./blocks/registry\";\nexport { BlockRenderer } from \"./blocks/BlockRenderer\";\nexport { RegistryProvider, useRegistry, BlockDataProvider, useBlockData } from \"./blocks/context\";\nexport { builtinBlocks, ConsentEmbed } from \"./blocks/builtins\";\nexport { PageRenderer } from \"./render/PageRenderer\";\nexport { PublishedPage } from \"./render/PublishedPage\";\n\n// Module-systeem (XiCMS 3.0): modules bundelen blocks + instellingen; per site aan/uit + te stylen.\nexport {\n defineModule,\n resolveModules,\n isModuleActive,\n ModulesProvider,\n useModules,\n useModuleActive,\n useModuleSettings,\n applyBlockOverrides,\n moduleIdForBlockType,\n builtinModules,\n validateModuleContract,\n assertModuleContract,\n useConsent,\n ConsentBanner,\n ConsentGate,\n ConsentReopenLink,\n type ConsentState,\n type ConsentSettings,\n type ConsentCategory,\n type ModuleDefinition,\n type ModuleSiteSetting,\n type ModulesSiteConfig,\n type Capability,\n type ResolvedModules,\n type BlockOverride,\n type BlockOverrides,\n} from \"./modules\";\n\n// Demo-lock\nexport { DemoLock } from \"./lock/DemoLock\";\n\n// Demo-bewerkmodus (lokaal-only) leeft op het subpad `@xiwebdesign/xicms-core/demo` — dit zijn\n// demo-store-internals die niet in de publieke runtime-API thuishoren (klantsites gebruiken ze nooit;\n// alleen de XiCMS.nl-showcase). DemoStudio wordt intern door XiCMSProvider gerenderd.\n\n// Firebase (client)\nexport {\n getFirebaseApp,\n getFirebaseAuth,\n getDb,\n getBucket,\n isFirebaseConfigured,\n} from \"./firebase/client\";\nexport { initAppCheck } from \"./firebase/appcheck\";\nexport { SiteChrome } from \"./SiteChrome\";\nexport { CORE_VERSION, CORE_PACKAGE, isNieuwer } from \"./version\";\n"],"mappings":"AAQA,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAO9B,SAAS,aAAa,gBAAgB;AAItC,SAAS,cAAc,wBAAwB;AAI/C,SAAS,KAAK,eAAe,mBAAmB,+BAA+B;AAE/E,cAAc;AACd;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB,iBAAiB;AAC1C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,SAAS,2BAA6C;AACtD,SAAS,wBAAwB;AAGjC,SAAS,aAAa,cAAc,aAAa,gBAAgB,gBAAgB,qBAAqB;AAEtG,SAAS,oBAAoB;AAE7B,SAAS,yBAAyB;AAElC,SAAS,+BAA+B;AACxC,SAAS,mBAAmB,gBAAgB;AAE5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQP,SAAS,cAAc,eAAe;AACtC,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAOP,SAAS,eAAe;AACxB,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAChC,SAAS,gBAAgB;AACzB,SAAS,UAAU,iBAAiB,eAAe;AAGnD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAMK;AACP,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB,aAAa,mBAAmB,oBAAoB;AAC/E,SAAS,eAAe,oBAAoB;AAC5C,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAG9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAWK;AAGP,SAAS,gBAAgB;AAOzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,cAAc,cAAc,iBAAiB;","names":[]}
|
|
@@ -30,6 +30,35 @@ export interface XicmsRewrite {
|
|
|
30
30
|
source: string;
|
|
31
31
|
destination: string;
|
|
32
32
|
}
|
|
33
|
+
export interface XicmsHeader {
|
|
34
|
+
source: string;
|
|
35
|
+
headers: {
|
|
36
|
+
key: string;
|
|
37
|
+
value: string;
|
|
38
|
+
}[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Headers die de Google-login-popup laten werken.
|
|
42
|
+
*
|
|
43
|
+
* `signInWithPopup` opent een venster naar Google, en Google zet daar zelf
|
|
44
|
+
* `Cross-Origin-Opener-Policy: same-origin` op. Zonder tegenmaatregel knipt de browser daarmee de
|
|
45
|
+
* band tussen dat venster en de pagina die het opende door. Gevolg, letterlijk waargenomen op
|
|
46
|
+
* christelijkkraamcadeau:
|
|
47
|
+
*
|
|
48
|
+
* Cross-Origin-Opener-Policy policy would block the window.closed call.
|
|
49
|
+
* Cross-Origin-Opener-Policy policy would block the window.close call.
|
|
50
|
+
*
|
|
51
|
+
* Google authenticeert dan gewoon — er staat een `code=` in de handler-URL — maar de popup kan zich
|
|
52
|
+
* niet sluiten en `pollUserCancellation` blijft eeuwig doorpollen. De belofte van
|
|
53
|
+
* `signInWithPopup` lost nooit op, dus de terugval op redirect komt er niet eens aan te pas. Voor
|
|
54
|
+
* de gebruiker: er gebeurt niets en de boel blijft draaien.
|
|
55
|
+
*
|
|
56
|
+
* `same-origin-allow-popups` zegt: vensters die IK open houden hun band met mij. Dat is precies
|
|
57
|
+
* genoeg, en strikter dan de standaard `unsafe-none` die we nu de facto hadden.
|
|
58
|
+
*
|
|
59
|
+
* Zie firebase/firebase-js-sdk#8541 en #8295, en vercel/next.js discussie #51135.
|
|
60
|
+
*/
|
|
61
|
+
export declare function xicmsAuthHeaders(): XicmsHeader[];
|
|
33
62
|
/**
|
|
34
63
|
* Let op de bestemming: die wordt afgeleid uit het PROJECT-ID, niet uit
|
|
35
64
|
* `NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN`. Dat is opzet. Zodra je hierboven overstapt op same-origin
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
function xicmsAuthHeaders() {
|
|
2
|
+
return [
|
|
3
|
+
{
|
|
4
|
+
// De hele site, niet alleen /login: het beheer opent dezelfde popup bij een sessie die
|
|
5
|
+
// verlopen is, en dat gebeurt op willekeurig welke pagina.
|
|
6
|
+
source: "/:path*",
|
|
7
|
+
headers: [{ key: "Cross-Origin-Opener-Policy", value: "same-origin-allow-popups" }]
|
|
8
|
+
}
|
|
9
|
+
];
|
|
10
|
+
}
|
|
1
11
|
function xicmsAuthRewrites() {
|
|
2
12
|
const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;
|
|
3
13
|
if (!projectId) return [];
|
|
@@ -15,6 +25,7 @@ function xicmsAuthRewrites() {
|
|
|
15
25
|
];
|
|
16
26
|
}
|
|
17
27
|
export {
|
|
28
|
+
xicmsAuthHeaders,
|
|
18
29
|
xicmsAuthRewrites
|
|
19
30
|
};
|
|
20
31
|
//# sourceMappingURL=authRewrites.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/next/authRewrites.ts"],"sourcesContent":["/**\n * Rewrites die de Firebase-auth-handler op het EIGEN domein van de site serveren.\n *\n * Waarom dit nodig is (en niet cosmetisch):\n *\n * Firebase wikkelt de Google-login af op `<projectId>.firebaseapp.com/__/auth/handler`. Zolang de\n * site op een ander domein draait — en dat doet elke klantsite: `klant.nl`, `klant.vercel.app` —\n * is dat een DERDE PARTIJ. De SDK bewaart daar de tussenstand van de login. Browsers geven die\n * opslag niet meer vrij aan een derde partij (Safari en Firefox al jaren, Chrome inmiddels ook),\n * en dus loopt de flow vast:\n *\n * popup geblokkeerd → terugval op signInWithRedirect → je gaat naar Google, komt terug →\n * getRedirectResult vindt niets → je staat weer op het inlogscherm, uitgelogd, ZONDER foutcode.\n *\n * Doodlopend voor de klant, en onzichtbaar in de logs. Gemeten op christelijkkraamcadeau.vercel.app:\n * `/__/auth/handler` gaf daar 404 en op firebaseapp.com 200 — de hele flow liep cross-origin.\n *\n * Met deze rewrites serveert de site `/__/auth/*` vanaf zijn eigen origin. Zet daarna\n * `NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN=1` zodat de client zijn eigen host als `authDomain`\n * gebruikt; dan is er geen derde partij meer en werken popup én redirect overal.\n *\n * Gebruik in next.config.ts:\n *\n * import { xicmsAuthRewrites } from \"@xiwebdesign/xicms-core/next\";\n * const nextConfig: NextConfig = {\n * async rewrites() { return xicmsAuthRewrites(); },\n * };\n */\n\nexport interface XicmsRewrite {\n source: string;\n destination: string;\n}\n\n/**\n * Let op de bestemming: die wordt afgeleid uit het PROJECT-ID, niet uit\n * `NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN`. Dat is opzet. Zodra je hierboven overstapt op same-origin\n * wijst `authDomain` naar de site zelf — een rewrite die daarheen doorstuurt zou dan naar zichzelf\n * wijzen en een oneindige lus opleveren. `<projectId>.firebaseapp.com` is en blijft het echte adres\n * van de handler, ongeacht wat er verder is ingesteld.\n */\nexport function xicmsAuthRewrites(): XicmsRewrite[] {\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!projectId) return []; // geen Firebase ingericht → niets door te sturen.\n\n const doel = `https://${projectId}.firebaseapp.com`;\n\n if (process.env.NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN !== \"1\") {\n // Wél de rewrite, níet de vlag: dan verandert er functioneel niets en blijft de login over de\n // derde partij lopen. Dat is precies de situatie die je dacht te hebben opgelost, dus zeg het\n // hardop tijdens de build in plaats van het stil te laten.\n console.warn(\n \"[xicms] xicmsAuthRewrites() staat aan, maar NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN is niet '1'. \" +\n \"De auth-handler wordt nu wel op het eigen domein geserveerd, maar de client gebruikt nog \" +\n `${process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN ?? \"de firebaseapp.com-domein\"} als authDomain. ` +\n \"Zet de variabele op '1' om de login same-origin te maken.\",\n );\n }\n\n return [\n { source: \"/__/auth/:path*\", destination: `${doel}/__/auth/:path*` },\n // De handler laadt zijn eigen SDK-init vanaf /__/firebase/*; zonder deze regel laadt de\n // pagina half en blijft de login hangen op een leeg venster.\n { source: \"/__/firebase/:path*\", destination: `${doel}/__/firebase/:path*` },\n ];\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/next/authRewrites.ts"],"sourcesContent":["/**\n * Rewrites die de Firebase-auth-handler op het EIGEN domein van de site serveren.\n *\n * Waarom dit nodig is (en niet cosmetisch):\n *\n * Firebase wikkelt de Google-login af op `<projectId>.firebaseapp.com/__/auth/handler`. Zolang de\n * site op een ander domein draait — en dat doet elke klantsite: `klant.nl`, `klant.vercel.app` —\n * is dat een DERDE PARTIJ. De SDK bewaart daar de tussenstand van de login. Browsers geven die\n * opslag niet meer vrij aan een derde partij (Safari en Firefox al jaren, Chrome inmiddels ook),\n * en dus loopt de flow vast:\n *\n * popup geblokkeerd → terugval op signInWithRedirect → je gaat naar Google, komt terug →\n * getRedirectResult vindt niets → je staat weer op het inlogscherm, uitgelogd, ZONDER foutcode.\n *\n * Doodlopend voor de klant, en onzichtbaar in de logs. Gemeten op christelijkkraamcadeau.vercel.app:\n * `/__/auth/handler` gaf daar 404 en op firebaseapp.com 200 — de hele flow liep cross-origin.\n *\n * Met deze rewrites serveert de site `/__/auth/*` vanaf zijn eigen origin. Zet daarna\n * `NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN=1` zodat de client zijn eigen host als `authDomain`\n * gebruikt; dan is er geen derde partij meer en werken popup én redirect overal.\n *\n * Gebruik in next.config.ts:\n *\n * import { xicmsAuthRewrites } from \"@xiwebdesign/xicms-core/next\";\n * const nextConfig: NextConfig = {\n * async rewrites() { return xicmsAuthRewrites(); },\n * };\n */\n\nexport interface XicmsRewrite {\n source: string;\n destination: string;\n}\n\nexport interface XicmsHeader {\n source: string;\n headers: { key: string; value: string }[];\n}\n\n/**\n * Headers die de Google-login-popup laten werken.\n *\n * `signInWithPopup` opent een venster naar Google, en Google zet daar zelf\n * `Cross-Origin-Opener-Policy: same-origin` op. Zonder tegenmaatregel knipt de browser daarmee de\n * band tussen dat venster en de pagina die het opende door. Gevolg, letterlijk waargenomen op\n * christelijkkraamcadeau:\n *\n * Cross-Origin-Opener-Policy policy would block the window.closed call.\n * Cross-Origin-Opener-Policy policy would block the window.close call.\n *\n * Google authenticeert dan gewoon — er staat een `code=` in de handler-URL — maar de popup kan zich\n * niet sluiten en `pollUserCancellation` blijft eeuwig doorpollen. De belofte van\n * `signInWithPopup` lost nooit op, dus de terugval op redirect komt er niet eens aan te pas. Voor\n * de gebruiker: er gebeurt niets en de boel blijft draaien.\n *\n * `same-origin-allow-popups` zegt: vensters die IK open houden hun band met mij. Dat is precies\n * genoeg, en strikter dan de standaard `unsafe-none` die we nu de facto hadden.\n *\n * Zie firebase/firebase-js-sdk#8541 en #8295, en vercel/next.js discussie #51135.\n */\nexport function xicmsAuthHeaders(): XicmsHeader[] {\n return [\n {\n // De hele site, niet alleen /login: het beheer opent dezelfde popup bij een sessie die\n // verlopen is, en dat gebeurt op willekeurig welke pagina.\n source: \"/:path*\",\n headers: [{ key: \"Cross-Origin-Opener-Policy\", value: \"same-origin-allow-popups\" }],\n },\n ];\n}\n\n/**\n * Let op de bestemming: die wordt afgeleid uit het PROJECT-ID, niet uit\n * `NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN`. Dat is opzet. Zodra je hierboven overstapt op same-origin\n * wijst `authDomain` naar de site zelf — een rewrite die daarheen doorstuurt zou dan naar zichzelf\n * wijzen en een oneindige lus opleveren. `<projectId>.firebaseapp.com` is en blijft het echte adres\n * van de handler, ongeacht wat er verder is ingesteld.\n */\nexport function xicmsAuthRewrites(): XicmsRewrite[] {\n const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;\n if (!projectId) return []; // geen Firebase ingericht → niets door te sturen.\n\n const doel = `https://${projectId}.firebaseapp.com`;\n\n if (process.env.NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN !== \"1\") {\n // Wél de rewrite, níet de vlag: dan verandert er functioneel niets en blijft de login over de\n // derde partij lopen. Dat is precies de situatie die je dacht te hebben opgelost, dus zeg het\n // hardop tijdens de build in plaats van het stil te laten.\n console.warn(\n \"[xicms] xicmsAuthRewrites() staat aan, maar NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN is niet '1'. \" +\n \"De auth-handler wordt nu wel op het eigen domein geserveerd, maar de client gebruikt nog \" +\n `${process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN ?? \"de firebaseapp.com-domein\"} als authDomain. ` +\n \"Zet de variabele op '1' om de login same-origin te maken.\",\n );\n }\n\n return [\n { source: \"/__/auth/:path*\", destination: `${doel}/__/auth/:path*` },\n // De handler laadt zijn eigen SDK-init vanaf /__/firebase/*; zonder deze regel laadt de\n // pagina half en blijft de login hangen op een leeg venster.\n { source: \"/__/firebase/:path*\", destination: `${doel}/__/firebase/:path*` },\n ];\n}\n"],"mappings":"AA4DO,SAAS,mBAAkC;AAChD,SAAO;AAAA,IACL;AAAA;AAAA;AAAA,MAGE,QAAQ;AAAA,MACR,SAAS,CAAC,EAAE,KAAK,8BAA8B,OAAO,2BAA2B,CAAC;AAAA,IACpF;AAAA,EACF;AACF;AASO,SAAS,oBAAoC;AAClD,QAAM,YAAY,QAAQ,IAAI;AAC9B,MAAI,CAAC,UAAW,QAAO,CAAC;AAExB,QAAM,OAAO,WAAW,SAAS;AAEjC,MAAI,QAAQ,IAAI,0CAA0C,KAAK;AAI7D,YAAQ;AAAA,MACN,2LAEK,QAAQ,IAAI,oCAAoC,2BAA2B;AAAA,IAElF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,EAAE,QAAQ,mBAAmB,aAAa,GAAG,IAAI,kBAAkB;AAAA;AAAA;AAAA,IAGnE,EAAE,QAAQ,uBAAuB,aAAa,GAAG,IAAI,sBAAsB;AAAA,EAC7E;AACF;","names":[]}
|
package/dist/next/index.cjs
CHANGED
|
@@ -20,11 +20,22 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/next/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
xicmsAuthHeaders: () => xicmsAuthHeaders,
|
|
23
24
|
xicmsAuthRewrites: () => xicmsAuthRewrites
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(index_exports);
|
|
26
27
|
|
|
27
28
|
// src/next/authRewrites.ts
|
|
29
|
+
function xicmsAuthHeaders() {
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
// De hele site, niet alleen /login: het beheer opent dezelfde popup bij een sessie die
|
|
33
|
+
// verlopen is, en dat gebeurt op willekeurig welke pagina.
|
|
34
|
+
source: "/:path*",
|
|
35
|
+
headers: [{ key: "Cross-Origin-Opener-Policy", value: "same-origin-allow-popups" }]
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
}
|
|
28
39
|
function xicmsAuthRewrites() {
|
|
29
40
|
const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;
|
|
30
41
|
if (!projectId) return [];
|
|
@@ -43,5 +54,6 @@ function xicmsAuthRewrites() {
|
|
|
43
54
|
}
|
|
44
55
|
// Annotate the CommonJS export names for ESM import in node:
|
|
45
56
|
0 && (module.exports = {
|
|
57
|
+
xicmsAuthHeaders,
|
|
46
58
|
xicmsAuthRewrites
|
|
47
59
|
});
|