Files
siftlode/frontend/src/lib/onboarding.ts
T
peter 18edbecb1e fix(auth): close the account pre-emption + wrong-account link holes (E1 S5, SEC-10/11)
SEC-10 (account pre-emption / "pre-hijack"): a password set on an account whose
email was never verified is untrusted — whoever registered the address (to
pre-empt the real owner) could have chosen it. Both the Google-adoption callback
and admin activation (_activate_user_by_email) now drop that pre-set password and
bump the session epoch when the account isn't email_verified, so a later Google
login / admin approval can't hand the pre-set password a working login. No-op for
a verified account (the legit no-SMTP flow). +2 tests.

SEC-11 (link/upgrade bound to the session default): the OAuth link/upgrade GETs
resolved to the session DEFAULT account, so a tab acting as a secondary account
would link Google / the YouTube grant to the wrong account. The frontend now
threads the active account through the navigation URL (withActiveAccount ->
?account=<active>), and _complete_link accepts any wallet account, not only the
default, so the identity lands on the account that started the flow.
2026-08-18 03:02:34 +02:00

48 lines
2.4 KiB
TypeScript

// Onboarding wizard visibility, kept stable across the OAuth redirect round-trip.
//
// Granting a YouTube scope navigates away to Google's consent screen and back, which
// fully reloads the SPA — so we can't hold the wizard's progress in React state. Instead
// we derive the current step from the granted scopes (me.can_read / me.can_write) and use
// two storage flags to decide whether to auto-open:
//
// ONBOARD_ACTIVE (sessionStorage) — set while the user is mid-flow (e.g. just clicked
// "Connect"); makes the wizard reopen after the
// redirect so they land on the next step.
// ONBOARD_DISMISSED (localStorage) — set when the user skips the essential read step;
// suppresses the auto-popup on future logins (they
// can still reopen it from Settings → Account).
import { withActiveAccount } from "./activeAccount";
import { getAccountRaw, LS, setAccountRaw } from "./storage";
const ONBOARD_ACTIVE = "siftlode.onboarding.active"; // sessionStorage (not in LS, which is localStorage)
// Per-account (see accountKey): a fresh account should still get the onboarding nudge even if a
// different account in the same browser dismissed it.
const ONBOARD_DISMISSED = LS.onboardingDismissed;
export function shouldAutoOpenOnboarding(me: {
can_read: boolean;
is_demo?: boolean;
google_enabled?: boolean;
}): boolean {
// The shared demo account can never connect YouTube, so never nudge it to.
if (me.is_demo) return false;
// YouTube connect needs Google OAuth configured on this instance. A self-host instance may run
// email+password only — then there's nothing to connect, so don't nudge.
if (me.google_enabled === false) return false;
if (sessionStorage.getItem(ONBOARD_ACTIVE)) return true;
return !me.can_read && !getAccountRaw(ONBOARD_DISMISSED);
}
/** Mark a grant as in progress, then send the user to Google's consent screen. */
export function beginGrant(access: "read" | "write"): void {
sessionStorage.setItem(ONBOARD_ACTIVE, "1");
window.location.href = withActiveAccount(`/auth/upgrade?access=${access}`);
}
/** Leave the wizard. `dismiss` suppresses the future auto-popup (used when read is skipped). */
export function endOnboarding(dismiss: boolean): void {
sessionStorage.removeItem(ONBOARD_ACTIVE);
if (dismiss) setAccountRaw(ONBOARD_DISMISSED, "1");
}