fix(auth): porter l'identité par param d'URL (?id=), pas localStorage; renommer username→identifier
Cause racine du décalage d'identité : l'app tourne dans DEUX contextes avec DEUX partitions de localStorage — top-level (127.0.0.1:3000 direct, barrière) et iframe (embarquée sous nextgraph.net après le round-trip broker). Le navigateur partitionne le storage par site top-level, donc l'identifiant saisi en top-level n'est jamais celui que l'app connectée lit dans l'iframe (symptôme: deux valeurs divergentes). Fix : le param d'URL ?id= devient la SOURCE DE VÉRITÉ. Le SDK redirige avec encodeURIComponent(window.location.href) (URL app complète, query comprise), donc un param d'URL TRAVERSE la frontière contrairement à localStorage. AuthGate écrit ?id=<identifiant> (replaceState) avant connect(); AccountContext résout par priorité (1) ?id= puis (2) localStorage (préremplissage same-partition seulement). Renommage username→identifier (champ useAccount, normalizeIdentifier, clé festipod.account.identifier) — c'est un id technique d'espace, pas un username. Le username de PROFIL (nom d'affichage) est laissé intact. Test garde-fou @ui (identifiant-resolution.feature) : la priorité param>localStorage, rouge si on l'inverse. Le flux de barrière étant désactivé en @e2e, ces @ui sont la seule couche qui le garde. Doctrine: knowledge_authentication (porteur URL + partition) + knowledge_context-internals (vocab). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+28
-10
@@ -15,7 +15,7 @@
|
||||
|
||||
import { useEffect, type ReactNode } from 'react';
|
||||
import { useNextGraph } from '../shared/context/NextGraphContext';
|
||||
import { useAccount } from '../shared/context/AccountContext';
|
||||
import { useAccount, normalizeIdentifier } from '../shared/context/AccountContext';
|
||||
import { AccessGateScreen } from '../modules/auth/screens/AccessGateScreen';
|
||||
import { useRouter, useNavigate } from './router';
|
||||
|
||||
@@ -27,7 +27,7 @@ const GATE_DISABLED = globalThis.__FESTIPOD_ACCESS_GATE_DISABLED__ === true;
|
||||
|
||||
export function AuthGate({ children }: { children: ReactNode }) {
|
||||
const { status, error, connect } = useNextGraph();
|
||||
const { username, login } = useAccount();
|
||||
const { identifier, login } = useAccount();
|
||||
const { route } = useRouter();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -36,10 +36,10 @@ export function AuthGate({ children }: { children: ReactNode }) {
|
||||
// round-trip), so on return the app can land on '/' with a session already
|
||||
// open; the removed ConnexionScreen used to do this navigate on login.
|
||||
useEffect(() => {
|
||||
if (!GATE_DISABLED && status === 'connected' && username && route.page === 'welcome') {
|
||||
if (!GATE_DISABLED && status === 'connected' && identifier && route.page === 'welcome') {
|
||||
navigate('/home');
|
||||
}
|
||||
}, [status, username, route.page, navigate]);
|
||||
}, [status, identifier, route.page, navigate]);
|
||||
|
||||
// Gate explicitly disabled (no-gate build / @e2e harness) → straight to app.
|
||||
if (GATE_DISABLED) {
|
||||
@@ -47,22 +47,40 @@ export function AuthGate({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
// Access barrier — shown until BOTH the wallet is open AND the space is named.
|
||||
// "Entrer" records the identifier (persisted immediately, so it survives the
|
||||
// broker redirect) and, if the wallet isn't open yet, triggers the connect.
|
||||
// "Entrer" records the identifier (persisted to localStorage AND written into
|
||||
// the `?id=` URL param, which is what actually survives the broker redirect
|
||||
// across the partitioned frontier) and, if the wallet isn't open yet, triggers
|
||||
// the connect.
|
||||
//
|
||||
// On return (reload / broker round-trip) the identifier is already stored, so
|
||||
// we PREFILL the field with it (`initialIdentifier`) — the user never sees a
|
||||
// bare empty prompt they must re-type. It is captured ONCE, at first access.
|
||||
if (status !== 'connected' || !username) {
|
||||
const onEnter = (identifier: string) => {
|
||||
login(identifier);
|
||||
if (status !== 'connected' || !identifier) {
|
||||
const onEnter = (entered: string) => {
|
||||
login(entered);
|
||||
// Carry the identifier across the broker frontier via the URL. localStorage
|
||||
// is partitioned by top-level site, so the value written here (127.0.0.1)
|
||||
// is NOT what the app reads inside the broker iframe (nextgraph.net). The
|
||||
// `@ng-org/web` redirect embeds the FULL app URL (query included) in the
|
||||
// broker `o=`, which is reloaded in the iframe — so writing the normalized
|
||||
// id into `?id=` BEFORE connect() makes it travel. `history.replaceState`
|
||||
// (not push) keeps a single history entry. See AccountContext resolution.
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('id', normalizeIdentifier(entered));
|
||||
window.history.replaceState(window.history.state, '', url.toString());
|
||||
} catch {
|
||||
/* URL construction can't fail for a real page URL; ignore defensively */
|
||||
}
|
||||
}
|
||||
if (status !== 'connected') connect();
|
||||
};
|
||||
return (
|
||||
<AccessGateScreen
|
||||
status={status}
|
||||
error={error}
|
||||
initialIdentifier={username ?? ''}
|
||||
initialIdentifier={identifier ?? ''}
|
||||
onEnter={onEnter}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user