mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:41:09 +02:00
feat(mana/web): global requireAuth() gate for guest-blocked features
The unified Mana app runs most modules in a "guest mode": you can
open a module, look around, type a quick note, etc. without an
account. But anything that touches an *encrypted* table (dreams
voice capture, memoro recordings, notes, todo, calendar events, …)
needs the user to be logged in — the encryption vault only unlocks
against a Mana Auth session, and writing to those tables without
it throws `VaultLockedError` at the very last step of the action.
Before this commit, every entry point into an encryption-required
action would silently let the guest go through the whole flow
(record audio, wait for transcription, open the dexie write) and
then explode with a stack-trace error. The user lost work and
didn't know why. The dreams voice capture flow surfaced this
during the 2026-04-08 STT debugging session.
The fix is a global imperative gate: `requireAuth({ feature, reason })`.
Call sites await it before the action; it returns immediately if the
user is already authenticated, otherwise pops a global modal that
asks the guest to log in or cancel. Promise-based, so callers
decide what to do with `false` (silent abort, restore state, own
toast).
$lib/auth/require-auth.svelte.ts new — store + helper
$lib/components/auth/AuthRequiredModal.svelte new — global modal
routes/+layout.svelte mount the modal once
packages/shared-utils/src/analytics.ts new ManaEvents.featureBlockedByAuth
event for conversion tracking
Wired into the two voice-capture entry points that actually exhibited
the bug:
modules/dreams/ListView.svelte → feature: 'dreams-voice-capture'
routes/(app)/memoro/+page.svelte → feature: 'memoro-voice-capture'
Both gate on `requireAuth()` BEFORE the mic permission request, so
guests see the friendly "Konto erforderlich" modal instead of
recording → transcribing → crashing.
Design choices documented in detail in the require-auth.svelte.ts
header comment:
- Imperative function (not a button wrapper component) so it
works in event handlers, store actions, keyboard shortcuts,
drag-drop handlers — anywhere async code runs.
- Single global modal mounted once in the root layout, no
portal/z-index gymnastics; two simultaneous prompts replace
each other (the most recent one wins).
- Checks `authStore.isAuthenticated`, not vault-unlocked state —
the user-facing concept is "I need an account", not "I need
a working encryption vault". Vault-unlock failures (network
error etc.) are a separate bug class with their own UX.
- The modal navigates to `/login?next=<current path>` so the
user lands back on the same page after logging in. The
Promise resolves `false` on navigation; the user re-clicks
the original button after coming back, and the second click
sees `isAuthenticated === true` and proceeds without a modal.
Re-triggering the original action across a navigation cycle
would require restoring half-recorded mic state — not worth
the complexity, and the second click is a clean UX.
How to wire a new entry point (4 lines):
import { requireAuth } from '$lib/auth/require-auth.svelte';
async function handleCreateThing() {
const ok = await requireAuth({
feature: 'create-thing',
reason: 'Things werden verschlüsselt gespeichert. Dafür brauchst du ein Mana-Konto.',
});
if (!ok) return;
// ...existing logic
}
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2b4494628e
commit
45958ad885
6 changed files with 356 additions and 2 deletions
|
|
@ -326,8 +326,7 @@ export const ManaEvents = {
|
|||
onboardingSkipped: (atStep: number) => track.mana('onboarding_skipped', { at_step: atStep }),
|
||||
dashboardEditToggled: (editing: boolean) => track.mana('dashboard_edit_toggled', { editing }),
|
||||
widgetAdded: (widgetType: string) => track.mana('widget_added', { widget_type: widgetType }),
|
||||
widgetRemoved: (widgetType: string) =>
|
||||
track.mana('widget_removed', { widget_type: widgetType }),
|
||||
widgetRemoved: (widgetType: string) => track.mana('widget_removed', { widget_type: widgetType }),
|
||||
widgetResized: (widgetType: string, size: string) =>
|
||||
track.mana('widget_resized', { widget_type: widgetType, size }),
|
||||
creditsTabViewed: (tab: string) => track.mana('credits_tab_viewed', { tab }),
|
||||
|
|
@ -342,6 +341,15 @@ export const ManaEvents = {
|
|||
secondModuleUsed: (appId: string) => track.mana('second_module_used', { app: appId }),
|
||||
/** Guest user converted to registered user */
|
||||
guestConverted: () => track.mana('guest_converted'),
|
||||
/**
|
||||
* A guest tried to use a feature that requires an account.
|
||||
* `feature` is the stable identifier passed to `requireAuth()`,
|
||||
* `action` is whether the user clicked through to login or
|
||||
* cancelled the modal — useful for measuring the conversion
|
||||
* rate of the auth gate.
|
||||
*/
|
||||
featureBlockedByAuth: (params: { feature: string; action: 'login' | 'cancel' }) =>
|
||||
track.mana('feature_blocked_by_auth', params),
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue