@xiwebdesign/xicms-core 7.3.0 → 7.4.0

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.
@@ -13,6 +13,22 @@ interface AuthState {
13
13
  signIn: (email: string, password: string) => Promise<void>;
14
14
  /** Inloggen met Google (popup). */
15
15
  signInWithGoogle: () => Promise<void>;
16
+ /**
17
+ * Stuur een inloglink naar dit adres (magic link). Geen wachtwoord, dus ook niets te vergeten —
18
+ * en anders dan de Google-flow raakt dit geen enkel domein van een derde partij, waardoor het
19
+ * werkt in browsers die opslag van derden blokkeren.
20
+ */
21
+ sendLoginLink: (email: string) => Promise<void>;
22
+ /**
23
+ * Er staat een geldige inloglink in de adresbalk, maar het bijbehorende adres is hier niet
24
+ * bekend — de link is op een ander apparaat geopend dan waar hij is aangevraagd. Het
25
+ * inlogscherm vraagt er dan om; zonder die vraag zou een geldige link doodlopen.
26
+ */
27
+ loginLinkNeedsEmail: boolean;
28
+ /** De link inwisselen met een handmatig opgegeven adres (zie `loginLinkNeedsEmail`). */
29
+ completeLoginLink: (email: string) => Promise<void>;
30
+ /** Ging het inwisselen van de link mis (verlopen, al gebruikt, ander adres)? */
31
+ loginLinkError: unknown;
16
32
  signOut: () => Promise<void>;
17
33
  /** Forceer het opnieuw inlezen van claims (na een rolwijziging). */
18
34
  refresh: () => Promise<void>;
@@ -11,6 +11,12 @@ import { hasAuthHint, isFirebaseConfigured, setAuthHint } from "../firebase/conf
11
11
  import { roleForSite } from "./roles.js";
12
12
  import { DEMO_EVENT, isDemoLoggedIn, setDemoLoggedIn } from "../demo/demoStore.js";
13
13
  const AuthContext = createContext(null);
14
+ const LINK_EMAIL_KEY = "xicms.loginlink.email";
15
+ function lijktOpInloglink() {
16
+ if (typeof window === "undefined") return false;
17
+ const q = window.location.search;
18
+ return q.includes("mode=signIn") && q.includes("oobCode=");
19
+ }
14
20
  function AuthProvider({
15
21
  siteId,
16
22
  demo = false,
@@ -21,6 +27,20 @@ function AuthProvider({
21
27
  const [user, setUser] = useState(null);
22
28
  const [claims, setClaims] = useState(null);
23
29
  const [demoLoggedIn, setDemoLoggedInState] = useState(false);
30
+ const [linkVraagtEmail, setLinkVraagtEmail] = useState(false);
31
+ const [linkFout, setLinkFout] = useState(null);
32
+ function opruimenNaLinkLogin() {
33
+ try {
34
+ window.localStorage.removeItem(LINK_EMAIL_KEY);
35
+ } catch {
36
+ }
37
+ setLinkVraagtEmail(false);
38
+ setLinkFout(null);
39
+ try {
40
+ window.history.replaceState({}, "", window.location.pathname + window.location.hash);
41
+ } catch {
42
+ }
43
+ }
24
44
  useEffect(() => {
25
45
  if (!demo) return;
26
46
  const sync = () => setDemoLoggedInState(isDemoLoggedIn());
@@ -46,7 +66,7 @@ function AuthProvider({
46
66
  });
47
67
  }
48
68
  useEffect(() => {
49
- if (!configured || !hasAuthHint()) {
69
+ if (!configured || !hasAuthHint() && !lijktOpInloglink()) {
50
70
  setLoading(false);
51
71
  return;
52
72
  }
@@ -61,6 +81,24 @@ function AuthProvider({
61
81
  void sdk.getRedirectResult(auth).catch((e) => {
62
82
  console.error("[xicms] inloggen via redirect mislukt:", e);
63
83
  });
84
+ if (sdk.isSignInWithEmailLink(auth, window.location.href)) {
85
+ let bewaard = null;
86
+ try {
87
+ bewaard = window.localStorage.getItem(LINK_EMAIL_KEY);
88
+ } catch {
89
+ }
90
+ if (bewaard) {
91
+ try {
92
+ await sdk.signInWithEmailLink(auth, bewaard, window.location.href);
93
+ opruimenNaLinkLogin();
94
+ } catch (e) {
95
+ console.error("[xicms] inloggen via link mislukt:", e);
96
+ setLinkFout(e);
97
+ }
98
+ } else if (!afgebroken) {
99
+ setLinkVraagtEmail(true);
100
+ }
101
+ }
64
102
  unsub = sdk.onAuthStateChanged(auth, async (u) => {
65
103
  setUser(u);
66
104
  setAuthHint(Boolean(u));
@@ -100,6 +138,24 @@ function AuthProvider({
100
138
  await sdk.signInWithRedirect(auth, provider);
101
139
  }
102
140
  },
141
+ async sendLoginLink(email) {
142
+ const { auth, sdk } = await authSdk();
143
+ const adres = email.trim();
144
+ const url = `${window.location.origin}${window.location.pathname}`;
145
+ await sdk.sendSignInLinkToEmail(auth, adres, { url, handleCodeInApp: true });
146
+ try {
147
+ window.localStorage.setItem(LINK_EMAIL_KEY, adres);
148
+ } catch {
149
+ }
150
+ },
151
+ loginLinkNeedsEmail: linkVraagtEmail,
152
+ loginLinkError: linkFout,
153
+ async completeLoginLink(email) {
154
+ const { auth, sdk } = await authSdk();
155
+ await sdk.signInWithEmailLink(auth, email.trim(), window.location.href);
156
+ opruimenNaLinkLogin();
157
+ setAuthHint(true);
158
+ },
103
159
  async signOut() {
104
160
  if (demo) {
105
161
  setDemoLoggedIn(false);
@@ -118,7 +174,7 @@ function AuthProvider({
118
174
  setDemoLoggedIn(true);
119
175
  }
120
176
  };
121
- }, [configured, loading, user, claims, siteId, demo, demoLoggedIn]);
177
+ }, [configured, loading, user, claims, siteId, demo, demoLoggedIn, linkVraagtEmail, linkFout]);
122
178
  return /* @__PURE__ */ jsx(AuthContext.Provider, { value, children });
123
179
  }
124
180
  function useAuth() {
@@ -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 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\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\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 if (!configured || !hasAuthHint()) {\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 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 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]);\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\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,4 +1,16 @@
1
- /** Eenvoudig inlogformulier voor beheerders/editors (e-mail/wachtwoord of Google). */
1
+ /**
2
+ * Inlogscherm voor beheerders/editors — wachtwoordloos van opzet.
3
+ *
4
+ * Twee wegen naar binnen: Google, of een inloglink per mail. Geen van beide kent een wachtwoord,
5
+ * en dus ook geen vergeten-wachtwoord-mailtjes. De inloglink is bovendien de betrouwbaarste van de
6
+ * twee: hij raakt geen enkel domein van een derde partij en werkt daarom ook in browsers die
7
+ * opslag van derden blokkeren — precies waar de Google-redirect op stukliep.
8
+ *
9
+ * Het wachtwoordformulier is niet weggegooid maar weggevouwen. Als beide wachtwoordloze wegen over
10
+ * e-mail lopen, is e-mail een enkelvoudig storingspunt: hapert de mailbezorging, dan komt niemand
11
+ * er meer in — ook de beheerder niet. Deze terugval blijft daarom bestaan voor wie hem nodig heeft,
12
+ * zonder hem aan iedereen op te dringen.
13
+ */
2
14
  export declare function LoginForm({ onSuccess }: {
3
15
  onSuccess?: () => void;
4
16
  }): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useState } from "react";
3
+ import { useEffect, useState } from "react";
4
4
  import { useAuth } from "./AuthProvider.js";
5
5
  function authErrorMessage(e, fallback) {
6
6
  const code = e?.code ?? "";
@@ -14,7 +14,7 @@ function authErrorMessage(e, fallback) {
14
14
  case "auth/configuration-not-found":
15
15
  return "Firebase Authentication is niet (juist) ingericht voor dit project. Controleer de NEXT_PUBLIC_FIREBASE_*-env en of Authentication aan staat.";
16
16
  case "auth/operation-not-allowed":
17
- return "Deze inlogmethode staat uit. Zet 'm aan in Firebase \u2192 Authentication \u2192 Sign-in method (E-mail/wachtwoord of Google).";
17
+ return "Deze inlogmethode staat uit. Zet 'm aan in Firebase \u2192 Authentication \u2192 Sign-in method (Google of E-mailkoppeling).";
18
18
  case "auth/network-request-failed":
19
19
  return "Geen verbinding met Firebase. Controleer je netwerk en de NEXT_PUBLIC_FIREBASE_*-config.";
20
20
  case "auth/invalid-credential":
@@ -26,16 +26,35 @@ function authErrorMessage(e, fallback) {
26
26
  case "auth/popup-closed-by-user":
27
27
  case "auth/cancelled-popup-request":
28
28
  return "De Google-login-popup werd geblokkeerd of gesloten. Sta popups toe en probeer opnieuw.";
29
+ // ── Inloglink ──────────────────────────────────────────────────────────────────────────
30
+ case "auth/expired-action-code":
31
+ return "Deze inloglink is verlopen. Vraag hieronder een nieuwe aan.";
32
+ case "auth/invalid-action-code":
33
+ return "Deze inloglink is al gebruikt of niet meer geldig. Vraag hieronder een nieuwe aan.";
29
34
  default:
30
35
  return code ? `${fallback} (${code})` : fallback;
31
36
  }
32
37
  }
33
38
  function LoginForm({ onSuccess }) {
34
- const { signIn, signInWithGoogle, configured, user } = useAuth();
39
+ const {
40
+ signIn,
41
+ signInWithGoogle,
42
+ sendLoginLink,
43
+ loginLinkNeedsEmail,
44
+ loginLinkError,
45
+ completeLoginLink,
46
+ configured,
47
+ user
48
+ } = useAuth();
35
49
  const [email, setEmail] = useState("");
36
50
  const [password, setPassword] = useState("");
37
51
  const [error, setError] = useState(null);
52
+ const [linkVerstuurd, setLinkVerstuurd] = useState(false);
53
+ const [wachtwoordModus, setWachtwoordModus] = useState(false);
38
54
  const [busy, setBusy] = useState(false);
55
+ useEffect(() => {
56
+ if (loginLinkError) setError(authErrorMessage(loginLinkError, "Inloggen via de link mislukt."));
57
+ }, [loginLinkError]);
39
58
  async function handleGoogle() {
40
59
  setError(null);
41
60
  setBusy(true);
@@ -62,21 +81,125 @@ function LoginForm({ onSuccess }) {
62
81
  "."
63
82
  ] }) });
64
83
  }
65
- async function handleSubmit(e) {
84
+ async function handleEmail(e) {
85
+ e.preventDefault();
86
+ setError(null);
87
+ setBusy(true);
88
+ try {
89
+ if (loginLinkNeedsEmail) {
90
+ await completeLoginLink(email);
91
+ onSuccess?.();
92
+ } else {
93
+ await sendLoginLink(email);
94
+ setLinkVerstuurd(true);
95
+ }
96
+ } catch (err) {
97
+ setError(authErrorMessage(err, "Versturen van de inloglink mislukt."));
98
+ } finally {
99
+ setBusy(false);
100
+ }
101
+ }
102
+ async function handleWachtwoord(e) {
66
103
  e.preventDefault();
67
104
  setError(null);
68
105
  setBusy(true);
69
106
  try {
70
107
  await signIn(email, password);
71
108
  onSuccess?.();
72
- } catch (e2) {
73
- setError(authErrorMessage(e2, "Inloggen mislukt. Controleer je e-mailadres en wachtwoord."));
109
+ } catch (err) {
110
+ setError(authErrorMessage(err, "Inloggen mislukt. Controleer je e-mailadres en wachtwoord."));
74
111
  } finally {
75
112
  setBusy(false);
76
113
  }
77
114
  }
78
- return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "xicms-card", style: { padding: "1.5rem", display: "grid", gap: "0.75rem", maxWidth: 360 }, children: [
115
+ const kaart = {
116
+ padding: "1.5rem",
117
+ display: "grid",
118
+ gap: "0.75rem",
119
+ maxWidth: 360
120
+ };
121
+ if (linkVerstuurd) {
122
+ return /* @__PURE__ */ jsxs("div", { className: "xicms-card", style: kaart, children: [
123
+ /* @__PURE__ */ jsx("h2", { style: { margin: 0 }, children: "Kijk in je mail" }),
124
+ /* @__PURE__ */ jsxs("p", { style: { margin: 0 }, children: [
125
+ "We stuurden een inloglink naar ",
126
+ /* @__PURE__ */ jsx("strong", { children: email }),
127
+ ". Klik erop en je bent binnen \u2014 geen wachtwoord nodig."
128
+ ] }),
129
+ /* @__PURE__ */ jsxs("p", { style: { margin: 0, fontSize: 13, opacity: 0.7 }, children: [
130
+ "Niets ontvangen? Kijk in je spam, of",
131
+ " ",
132
+ /* @__PURE__ */ jsx(
133
+ "button",
134
+ {
135
+ type: "button",
136
+ onClick: () => setLinkVerstuurd(false),
137
+ className: "xicms-btn xicms-btn--ghost",
138
+ style: { padding: 0, border: 0, background: "none", textDecoration: "underline", cursor: "pointer" },
139
+ children: "probeer een ander adres"
140
+ }
141
+ ),
142
+ "."
143
+ ] })
144
+ ] });
145
+ }
146
+ if (wachtwoordModus) {
147
+ return /* @__PURE__ */ jsxs("form", { onSubmit: handleWachtwoord, className: "xicms-card", style: kaart, children: [
148
+ /* @__PURE__ */ jsx("h2", { style: { margin: 0 }, children: "Inloggen met wachtwoord" }),
149
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: "0.25rem" }, children: [
150
+ /* @__PURE__ */ jsx("span", { children: "E-mailadres" }),
151
+ /* @__PURE__ */ jsx(
152
+ "input",
153
+ {
154
+ type: "email",
155
+ required: true,
156
+ value: email,
157
+ onChange: (e) => setEmail(e.target.value),
158
+ autoComplete: "email",
159
+ className: "xicms-input"
160
+ }
161
+ )
162
+ ] }),
163
+ /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: "0.25rem" }, children: [
164
+ /* @__PURE__ */ jsx("span", { children: "Wachtwoord" }),
165
+ /* @__PURE__ */ jsx(
166
+ "input",
167
+ {
168
+ type: "password",
169
+ required: true,
170
+ value: password,
171
+ onChange: (e) => setPassword(e.target.value),
172
+ autoComplete: "current-password",
173
+ className: "xicms-input"
174
+ }
175
+ )
176
+ ] }),
177
+ error && /* @__PURE__ */ jsx("p", { style: { color: "#dc2626", margin: 0 }, children: error }),
178
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: busy, className: "xicms-btn", children: busy ? "Bezig\u2026" : "Inloggen" }),
179
+ /* @__PURE__ */ jsx(
180
+ "button",
181
+ {
182
+ type: "button",
183
+ onClick: () => {
184
+ setWachtwoordModus(false);
185
+ setError(null);
186
+ },
187
+ className: "xicms-btn xicms-btn--ghost",
188
+ children: "Liever zonder wachtwoord"
189
+ }
190
+ )
191
+ ] });
192
+ }
193
+ return /* @__PURE__ */ jsxs("form", { onSubmit: handleEmail, className: "xicms-card", style: kaart, children: [
79
194
  /* @__PURE__ */ jsx("h2", { style: { margin: 0 }, children: "Inloggen" }),
195
+ loginLinkNeedsEmail && /* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: 14 }, children: "Bevestig even bij welk e-mailadres deze inloglink hoort. Dat is wat de link beveiligt: wie hem onderschept maar het adres niet weet, komt er niet in." }),
196
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: handleGoogle, disabled: busy, className: "xicms-btn", children: "Inloggen met Google" }),
197
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem", opacity: 0.6, fontSize: 13 }, children: [
198
+ /* @__PURE__ */ jsx("span", { style: { flex: 1, height: 1, background: "currentColor" } }),
199
+ " of",
200
+ " ",
201
+ /* @__PURE__ */ jsx("span", { style: { flex: 1, height: 1, background: "currentColor" } })
202
+ ] }),
80
203
  /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: "0.25rem" }, children: [
81
204
  /* @__PURE__ */ jsx("span", { children: "E-mailadres" }),
82
205
  /* @__PURE__ */ jsx(
@@ -91,29 +214,30 @@ function LoginForm({ onSuccess }) {
91
214
  }
92
215
  )
93
216
  ] }),
94
- /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: "0.25rem" }, children: [
95
- /* @__PURE__ */ jsx("span", { children: "Wachtwoord" }),
96
- /* @__PURE__ */ jsx(
97
- "input",
98
- {
99
- type: "password",
100
- required: true,
101
- value: password,
102
- onChange: (e) => setPassword(e.target.value),
103
- autoComplete: "current-password",
104
- className: "xicms-input"
105
- }
106
- )
107
- ] }),
108
217
  error && /* @__PURE__ */ jsx("p", { style: { color: "#dc2626", margin: 0 }, children: error }),
109
- /* @__PURE__ */ jsx("button", { type: "submit", disabled: busy, className: "xicms-btn", children: busy ? "Bezig\u2026" : "Inloggen" }),
110
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem", opacity: 0.6, fontSize: 13 }, children: [
111
- /* @__PURE__ */ jsx("span", { style: { flex: 1, height: 1, background: "currentColor" } }),
112
- " of",
113
- " ",
114
- /* @__PURE__ */ jsx("span", { style: { flex: 1, height: 1, background: "currentColor" } })
115
- ] }),
116
- /* @__PURE__ */ jsx("button", { type: "button", onClick: handleGoogle, disabled: busy, className: "xicms-btn xicms-btn--ghost", children: "Inloggen met Google" })
218
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: busy, className: "xicms-btn xicms-btn--ghost", children: busy ? "Bezig\u2026" : loginLinkNeedsEmail ? "Inloggen afronden" : "Stuur mij een inloglink" }),
219
+ !loginLinkNeedsEmail && /* @__PURE__ */ jsx(
220
+ "button",
221
+ {
222
+ type: "button",
223
+ onClick: () => {
224
+ setWachtwoordModus(true);
225
+ setError(null);
226
+ },
227
+ style: {
228
+ justifySelf: "start",
229
+ padding: 0,
230
+ border: 0,
231
+ background: "none",
232
+ font: "inherit",
233
+ fontSize: 12,
234
+ opacity: 0.55,
235
+ textDecoration: "underline",
236
+ cursor: "pointer"
237
+ },
238
+ children: "Inloggen met wachtwoord"
239
+ }
240
+ )
117
241
  ] });
118
242
  }
119
243
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/LoginForm.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState, 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 (E-mail/wachtwoord of Google).\";\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 default:\n return code ? `${fallback} (${code})` : fallback;\n }\n}\n\n/** Eenvoudig inlogformulier voor beheerders/editors (e-mail/wachtwoord of Google). */\nexport function LoginForm({ onSuccess }: { onSuccess?: () => void }) {\n const { signIn, signInWithGoogle, configured, user } = useAuth();\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n const [busy, setBusy] = useState(false);\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 async function handleSubmit(e: FormEvent) {\n e.preventDefault();\n setError(null);\n setBusy(true);\n try {\n await signIn(email, password);\n onSuccess?.();\n } catch (e) {\n setError(authErrorMessage(e, \"Inloggen mislukt. Controleer je e-mailadres en wachtwoord.\"));\n } finally {\n setBusy(false);\n }\n }\n\n return (\n <form onSubmit={handleSubmit} className=\"xicms-card\" style={{ padding: \"1.5rem\", display: \"grid\", gap: \"0.75rem\", maxWidth: 360 }}>\n <h2 style={{ margin: 0 }}>Inloggen</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 <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 <button type=\"button\" onClick={handleGoogle} disabled={busy} className=\"xicms-btn xicms-btn--ghost\">\n Inloggen met Google\n </button>\n </form>\n );\n}\n"],"mappings":"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","names":["e"]}
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,13 +1,3 @@
1
- /**
2
- * Firebase-CONFIGURATIE zonder de SDK.
3
- *
4
- * `firebase/client.ts` importeert `firebase/app`, `-auth`, `-firestore` en `-storage`. Elke module
5
- * die hem statisch importeerde trok die keten mee de PUBLIEKE bundel in — ~1 MB JavaScript voor
6
- * iedere bezoeker, terwijl die de site uit de content-bundle leest en nooit met Firebase praat.
7
- *
8
- * Alles wat alleen de env hoeft te lezen staat daarom hier. `client.ts` en de SDK-modules worden
9
- * voortaan DYNAMISCH geïmporteerd, op het moment dat er echt iets met Firebase moet gebeuren.
10
- */
11
1
  export declare function readFirebaseConfig(): {
12
2
  apiKey: string | undefined;
13
3
  authDomain: string | undefined;
@@ -1,7 +1,13 @@
1
+ function authDomain() {
2
+ const env = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN;
3
+ if (process.env.NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN !== "1") return env;
4
+ if (typeof window === "undefined") return env;
5
+ return window.location.host;
6
+ }
1
7
  function readFirebaseConfig() {
2
8
  return {
3
9
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
4
- authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
10
+ authDomain: authDomain(),
5
11
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
6
12
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
7
13
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/firebase/config.ts"],"sourcesContent":["/**\n * Firebase-CONFIGURATIE zonder de SDK.\n *\n * `firebase/client.ts` importeert `firebase/app`, `-auth`, `-firestore` en `-storage`. Elke module\n * die hem statisch importeerde trok die keten mee de PUBLIEKE bundel in — ~1 MB JavaScript voor\n * iedere bezoeker, terwijl die de site uit de content-bundle leest en nooit met Firebase praat.\n *\n * Alles wat alleen de env hoeft te lezen staat daarom hier. `client.ts` en de SDK-modules worden\n * voortaan DYNAMISCH geïmporteerd, op het moment dat er echt iets met Firebase moet gebeuren.\n */\nexport function readFirebaseConfig() {\n return {\n apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,\n authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,\n projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,\n storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,\n messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,\n appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,\n };\n}\n\n/** Is Firebase ingericht? Puur env — laadt geen SDK. */\nexport function isFirebaseConfigured(): boolean {\n const cfg = readFirebaseConfig();\n return Boolean(cfg.apiKey && cfg.projectId);\n}\n\n/**\n * Onthoudt dát er een Firebase-sessie is, zodat een anonieme bezoeker de auth-SDK niet hoeft te\n * downloaden om te ontdekken dat hij niet is ingelogd.\n *\n * Verlies van de vlag is onschuldig: de bezoeker lijkt dan uitgelogd, en zodra hij inlogt of het\n * beheer opent wordt de SDK alsnog geladen — `onAuthStateChanged` vindt de bestaande sessie dan\n * meteen terug, zonder wachtwoord.\n */\nexport const AUTH_HINT_KEY = \"xicms.auth\";\n\nexport function hasAuthHint(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return window.localStorage.getItem(AUTH_HINT_KEY) === \"1\";\n } catch {\n // Opslag geweigerd (private mode): dan maar laden — liever een zwaardere pagina dan een\n // editor die niet bij zijn beheer kan.\n return true;\n }\n}\n\nexport function setAuthHint(active: boolean): void {\n if (typeof window === \"undefined\") return;\n try {\n if (active) window.localStorage.setItem(AUTH_HINT_KEY, \"1\");\n else window.localStorage.removeItem(AUTH_HINT_KEY);\n } catch {\n /* opslag onbeschikbaar: hint vervalt, gedrag blijft correct (zie hasAuthHint) */\n }\n}\n"],"mappings":"AAUO,SAAS,qBAAqB;AACnC,SAAO;AAAA,IACL,QAAQ,QAAQ,IAAI;AAAA,IACpB,YAAY,QAAQ,IAAI;AAAA,IACxB,WAAW,QAAQ,IAAI;AAAA,IACvB,eAAe,QAAQ,IAAI;AAAA,IAC3B,mBAAmB,QAAQ,IAAI;AAAA,IAC/B,OAAO,QAAQ,IAAI;AAAA,EACrB;AACF;AAGO,SAAS,uBAAgC;AAC9C,QAAM,MAAM,mBAAmB;AAC/B,SAAO,QAAQ,IAAI,UAAU,IAAI,SAAS;AAC5C;AAUO,MAAM,gBAAgB;AAEtB,SAAS,cAAuB;AACrC,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,WAAO,OAAO,aAAa,QAAQ,aAAa,MAAM;AAAA,EACxD,QAAQ;AAGN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,YAAY,QAAuB;AACjD,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,QAAI,OAAQ,QAAO,aAAa,QAAQ,eAAe,GAAG;AAAA,QACrD,QAAO,aAAa,WAAW,aAAa;AAAA,EACnD,QAAQ;AAAA,EAER;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/firebase/config.ts"],"sourcesContent":["/**\n * Firebase-CONFIGURATIE zonder de SDK.\n *\n * `firebase/client.ts` importeert `firebase/app`, `-auth`, `-firestore` en `-storage`. Elke module\n * die hem statisch importeerde trok die keten mee de PUBLIEKE bundel in — ~1 MB JavaScript voor\n * iedere bezoeker, terwijl die de site uit de content-bundle leest en nooit met Firebase praat.\n *\n * Alles wat alleen de env hoeft te lezen staat daarom hier. `client.ts` en de SDK-modules worden\n * voortaan DYNAMISCH geïmporteerd, op het moment dat er echt iets met Firebase moet gebeuren.\n */\n/**\n * Welk domein wikkelt de login af?\n *\n * Standaard `<projectId>.firebaseapp.com` — een ANDER domein dan de site, en daarmee voor de\n * browser een derde partij. Sinds browsers opslag van derde partijen blokkeren komt een login via\n * `signInWithRedirect` dan terug zonder sessie en zonder foutcode: je staat weer op het\n * inlogscherm en niets vertelt je waarom (zie next/authRewrites.ts voor de meting).\n *\n * Staat `NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN` op \"1\", dan serveert de site de auth-handler zelf\n * (via `xicmsAuthRewrites()` in next.config) en gebruiken we de HUIDIGE host. Alles speelt zich dan\n * op één origin af en er is geen derde partij meer in het spel.\n *\n * De naam wordt letterlijk uitgeschreven en niet via een variabele opgezocht: Next vervangt\n * `process.env.NEXT_PUBLIC_*` alleen bij een letterlijke sleutel. Een berekende sleutel levert\n * `undefined` op in de browser — stil, en pas merkbaar als de login het niet doet.\n */\nfunction authDomain(): string | undefined {\n const env = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN;\n if (process.env.NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN !== \"1\") return env;\n // Tijdens SSR bestaat er geen host; val dan terug op de env. Auth draait uitsluitend in de\n // browser, dus de waarde die telt wordt altijd client-side bepaald.\n if (typeof window === \"undefined\") return env;\n return window.location.host;\n}\n\nexport function readFirebaseConfig() {\n return {\n apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,\n authDomain: authDomain(),\n projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,\n storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,\n messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,\n appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,\n };\n}\n\n/** Is Firebase ingericht? Puur env — laadt geen SDK. */\nexport function isFirebaseConfigured(): boolean {\n const cfg = readFirebaseConfig();\n return Boolean(cfg.apiKey && cfg.projectId);\n}\n\n/**\n * Onthoudt dát er een Firebase-sessie is, zodat een anonieme bezoeker de auth-SDK niet hoeft te\n * downloaden om te ontdekken dat hij niet is ingelogd.\n *\n * Verlies van de vlag is onschuldig: de bezoeker lijkt dan uitgelogd, en zodra hij inlogt of het\n * beheer opent wordt de SDK alsnog geladen — `onAuthStateChanged` vindt de bestaande sessie dan\n * meteen terug, zonder wachtwoord.\n */\nexport const AUTH_HINT_KEY = \"xicms.auth\";\n\nexport function hasAuthHint(): boolean {\n if (typeof window === \"undefined\") return false;\n try {\n return window.localStorage.getItem(AUTH_HINT_KEY) === \"1\";\n } catch {\n // Opslag geweigerd (private mode): dan maar laden — liever een zwaardere pagina dan een\n // editor die niet bij zijn beheer kan.\n return true;\n }\n}\n\nexport function setAuthHint(active: boolean): void {\n if (typeof window === \"undefined\") return;\n try {\n if (active) window.localStorage.setItem(AUTH_HINT_KEY, \"1\");\n else window.localStorage.removeItem(AUTH_HINT_KEY);\n } catch {\n /* opslag onbeschikbaar: hint vervalt, gedrag blijft correct (zie hasAuthHint) */\n }\n}\n"],"mappings":"AA0BA,SAAS,aAAiC;AACxC,QAAM,MAAM,QAAQ,IAAI;AACxB,MAAI,QAAQ,IAAI,0CAA0C,IAAK,QAAO;AAGtE,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,SAAO,OAAO,SAAS;AACzB;AAEO,SAAS,qBAAqB;AACnC,SAAO;AAAA,IACL,QAAQ,QAAQ,IAAI;AAAA,IACpB,YAAY,WAAW;AAAA,IACvB,WAAW,QAAQ,IAAI;AAAA,IACvB,eAAe,QAAQ,IAAI;AAAA,IAC3B,mBAAmB,QAAQ,IAAI;AAAA,IAC/B,OAAO,QAAQ,IAAI;AAAA,EACrB;AACF;AAGO,SAAS,uBAAgC;AAC9C,QAAM,MAAM,mBAAmB;AAC/B,SAAO,QAAQ,IAAI,UAAU,IAAI,SAAS;AAC5C;AAUO,MAAM,gBAAgB;AAEtB,SAAS,cAAuB;AACrC,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,WAAO,OAAO,aAAa,QAAQ,aAAa,MAAM;AAAA,EACxD,QAAQ;AAGN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,YAAY,QAAuB;AACjD,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,QAAI,OAAQ,QAAO,aAAa,QAAQ,eAAe,GAAG;AAAA,QACrD,QAAO,aAAa,WAAW,aAAa;AAAA,EACnD,QAAQ;AAAA,EAER;AACF;","names":[]}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Rewrites die de Firebase-auth-handler op het EIGEN domein van de site serveren.
3
+ *
4
+ * Waarom dit nodig is (en niet cosmetisch):
5
+ *
6
+ * Firebase wikkelt de Google-login af op `<projectId>.firebaseapp.com/__/auth/handler`. Zolang de
7
+ * site op een ander domein draait — en dat doet elke klantsite: `klant.nl`, `klant.vercel.app` —
8
+ * is dat een DERDE PARTIJ. De SDK bewaart daar de tussenstand van de login. Browsers geven die
9
+ * opslag niet meer vrij aan een derde partij (Safari en Firefox al jaren, Chrome inmiddels ook),
10
+ * en dus loopt de flow vast:
11
+ *
12
+ * popup geblokkeerd → terugval op signInWithRedirect → je gaat naar Google, komt terug →
13
+ * getRedirectResult vindt niets → je staat weer op het inlogscherm, uitgelogd, ZONDER foutcode.
14
+ *
15
+ * Doodlopend voor de klant, en onzichtbaar in de logs. Gemeten op christelijkkraamcadeau.vercel.app:
16
+ * `/__/auth/handler` gaf daar 404 en op firebaseapp.com 200 — de hele flow liep cross-origin.
17
+ *
18
+ * Met deze rewrites serveert de site `/__/auth/*` vanaf zijn eigen origin. Zet daarna
19
+ * `NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN=1` zodat de client zijn eigen host als `authDomain`
20
+ * gebruikt; dan is er geen derde partij meer en werken popup én redirect overal.
21
+ *
22
+ * Gebruik in next.config.ts:
23
+ *
24
+ * import { xicmsAuthRewrites } from "@xiwebdesign/xicms-core/next";
25
+ * const nextConfig: NextConfig = {
26
+ * async rewrites() { return xicmsAuthRewrites(); },
27
+ * };
28
+ */
29
+ export interface XicmsRewrite {
30
+ source: string;
31
+ destination: string;
32
+ }
33
+ /**
34
+ * Let op de bestemming: die wordt afgeleid uit het PROJECT-ID, niet uit
35
+ * `NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN`. Dat is opzet. Zodra je hierboven overstapt op same-origin
36
+ * wijst `authDomain` naar de site zelf — een rewrite die daarheen doorstuurt zou dan naar zichzelf
37
+ * wijzen en een oneindige lus opleveren. `<projectId>.firebaseapp.com` is en blijft het echte adres
38
+ * van de handler, ongeacht wat er verder is ingesteld.
39
+ */
40
+ export declare function xicmsAuthRewrites(): XicmsRewrite[];
@@ -0,0 +1,20 @@
1
+ function xicmsAuthRewrites() {
2
+ const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;
3
+ if (!projectId) return [];
4
+ const doel = `https://${projectId}.firebaseapp.com`;
5
+ if (process.env.NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN !== "1") {
6
+ console.warn(
7
+ `[xicms] xicmsAuthRewrites() staat aan, maar NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN is niet '1'. De auth-handler wordt nu wel op het eigen domein geserveerd, maar de client gebruikt nog ${process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN ?? "de firebaseapp.com-domein"} als authDomain. Zet de variabele op '1' om de login same-origin te maken.`
8
+ );
9
+ }
10
+ return [
11
+ { source: "/__/auth/:path*", destination: `${doel}/__/auth/:path*` },
12
+ // De handler laadt zijn eigen SDK-init vanaf /__/firebase/*; zonder deze regel laadt de
13
+ // pagina half en blijft de login hangen op een leeg venster.
14
+ { source: "/__/firebase/:path*", destination: `${doel}/__/firebase/:path*` }
15
+ ];
16
+ }
17
+ export {
18
+ xicmsAuthRewrites
19
+ };
20
+ //# sourceMappingURL=authRewrites.js.map
@@ -0,0 +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":"AAyCO,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":[]}
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/next/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ xicmsAuthRewrites: () => xicmsAuthRewrites
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/next/authRewrites.ts
28
+ function xicmsAuthRewrites() {
29
+ const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;
30
+ if (!projectId) return [];
31
+ const doel = `https://${projectId}.firebaseapp.com`;
32
+ if (process.env.NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN !== "1") {
33
+ console.warn(
34
+ `[xicms] xicmsAuthRewrites() staat aan, maar NEXT_PUBLIC_FIREBASE_AUTH_SAME_ORIGIN is niet '1'. De auth-handler wordt nu wel op het eigen domein geserveerd, maar de client gebruikt nog ${process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN ?? "de firebaseapp.com-domein"} als authDomain. Zet de variabele op '1' om de login same-origin te maken.`
35
+ );
36
+ }
37
+ return [
38
+ { source: "/__/auth/:path*", destination: `${doel}/__/auth/:path*` },
39
+ // De handler laadt zijn eigen SDK-init vanaf /__/firebase/*; zonder deze regel laadt de
40
+ // pagina half en blijft de login hangen op een leeg venster.
41
+ { source: "/__/firebase/:path*", destination: `${doel}/__/firebase/:path*` }
42
+ ];
43
+ }
44
+ // Annotate the CommonJS export names for ESM import in node:
45
+ 0 && (module.exports = {
46
+ xicmsAuthRewrites
47
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Helpers voor `next.config.ts`. Bewust een eigen ingang: dit draait in Node tijdens de build,
3
+ * niet in de browser, en mag dus niets uit de React-bundel meetrekken.
4
+ */
5
+ export { xicmsAuthRewrites, type XicmsRewrite } from "./authRewrites.js";
@@ -0,0 +1,5 @@
1
+ import { xicmsAuthRewrites } from "./authRewrites.js";
2
+ export {
3
+ xicmsAuthRewrites
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/next/index.ts"],"sourcesContent":["/**\n * Helpers voor `next.config.ts`. Bewust een eigen ingang: dit draait in Node tijdens de build,\n * niet in de browser, en mag dus niets uit de React-bundel meetrekken.\n */\nexport { xicmsAuthRewrites, type XicmsRewrite } from \"./authRewrites\";\n"],"mappings":"AAIA,SAAS,yBAA4C;","names":[]}
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const CORE_VERSION = true ? "7.3.0" : "0.0.0-dev";
1
+ const CORE_VERSION = true ? "7.4.0" : "0.0.0-dev";
2
2
  const CORE_PACKAGE = "@xiwebdesign/xicms-core";
3
3
  function isNieuwer(beschikbaar, huidig) {
4
4
  const delen = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiwebdesign/xicms-core",
3
- "version": "7.3.0",
3
+ "version": "7.4.0",
4
4
  "description": "XiCMS engine: gestructureerde content, pagina's, draft/publish, admin (TipTap+Radix), SEO en beveiliging voor klantsites.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -60,6 +60,11 @@
60
60
  "types": "./dist/server/index.d.ts",
61
61
  "import": "./dist/server/index.js"
62
62
  },
63
+ "./next": {
64
+ "types": "./dist/next/index.d.ts",
65
+ "import": "./dist/next/index.js",
66
+ "require": "./dist/next/index.cjs"
67
+ },
63
68
  "./layers.css": "./dist/layers.css",
64
69
  "./styles.css": "./dist/styles.css",
65
70
  "./admin.css": "./dist/admin.css",
@@ -80,7 +85,7 @@
80
85
  "access": "public"
81
86
  },
82
87
  "scripts": {
83
- "build": "tsup && tsc -p tsconfig.build.json && tailwindcss -i src/admin/admin.css -o dist/admin.css --minify && node ./scripts/copy-css.mjs && node ./scripts/add-extensions.mjs",
88
+ "build": "tsup && tsc -p tsconfig.build.json && tailwindcss -i src/admin/admin.css -o dist/admin.css --minify && node ./scripts/copy-css.mjs && node ./scripts/add-extensions.mjs && node ./scripts/build-next-cjs.mjs",
84
89
  "typecheck": "tsc --noEmit",
85
90
  "test": "vitest run",
86
91
  "prepublishOnly": "npm run build"