diff --git a/web/src/lib/api/client.ts b/web/src/lib/api/client.ts index 35d5e72a..2f88b250 100644 --- a/web/src/lib/api/client.ts +++ b/web/src/lib/api/client.ts @@ -423,7 +423,13 @@ export interface AuthSession { // Absent on older servers and default false on fresh instances — treat // absent as false. webmcp_enabled?: boolean; - user?: { id: string; email: string; username: string; name: string; role: string; plan?: string }; + // email_verified is false ONLY for a Pad Cloud self-serve signup that + // hasn't confirmed its email yet (PLAN-1933 DR-3). OAuth / invited / + // admin-created / pre-existing accounts are verified, and self-hosted + // instances never emit an unverified user. Absent on older servers — + // authStore.emailVerified treats absent as TRUE so the verification + // banner never shows on self-host or before the field lands. + user?: { id: string; email: string; username: string; name: string; role: string; plan?: string; email_verified?: boolean }; } // ── WebMCP tool-surface (PLAN-1888 / TASK-1892) ──────────────────────────── @@ -1534,7 +1540,7 @@ export const api = { body: JSON.stringify({ challenge_token: challengeToken, code: code || undefined, recovery_code: recoveryCode || undefined }) }), register: (email: string, name: string, password: string, username?: string, invitation_code?: string) => - request<{ user: { id: string; email: string; username: string; name: string; role: string }; token: string }>('/auth/register', { + request<{ user: { id: string; email: string; username: string; name: string; role: string; email_verified?: boolean }; token: string }>('/auth/register', { method: 'POST', body: JSON.stringify({ email, name, password, ...(username ? { username } : {}), ...(invitation_code ? { invitation_code } : {}) }) }), @@ -1573,6 +1579,17 @@ export const api = { method: 'POST', body: JSON.stringify({ email }) }), + // Re-send the email-verification link for an unverified Pad Cloud + // account (PLAN-1933 DR-5 / TASK-1940). Enumeration-safe: the server + // always returns 200 with the same body whether or not the address + // maps to an unverified account, so callers should show a neutral + // "if your account still needs verification, a link was sent" + // confirmation rather than branching on the response. + resendVerification: (email: string) => + request<{ ok: boolean; message: string }>('/auth/resend-verification', { + method: 'POST', + body: JSON.stringify({ email }) + }), resetPassword: (token: string, password: string) => request<{ ok: boolean; user: { id: string; email: string; username: string; name: string; role: string }; token: string }>('/auth/reset-password', { method: 'POST', diff --git a/web/src/lib/components/VerifyEmailBanner.svelte b/web/src/lib/components/VerifyEmailBanner.svelte new file mode 100644 index 00000000..4f98e1ed --- /dev/null +++ b/web/src/lib/components/VerifyEmailBanner.svelte @@ -0,0 +1,128 @@ + + +{#if visible} +
+{/if} + + diff --git a/web/src/lib/stores/auth.svelte.ts b/web/src/lib/stores/auth.svelte.ts index a3760f94..ef342638 100644 --- a/web/src/lib/stores/auth.svelte.ts +++ b/web/src/lib/stores/auth.svelte.ts @@ -35,6 +35,14 @@ export const authStore = { // unless the server explicitly says email is off. The /forgot-password // page swaps to host-recovery guidance when this is false. get emailConfigured() { return session?.email_configured ?? true; }, + // emailVerified is false ONLY for a Pad Cloud self-serve signup that hasn't + // confirmed its email yet (PLAN-1933 DR-3 / TASK-1940). Defaults to TRUE + // when the field is absent — older servers, self-hosted instances (which + // never mint unverified users), OAuth/invited/admin-created accounts, and + // the pre-load window. This default is load-bearing: the verification + // banner gates on `!emailVerified`, so a missing field or self-host must + // NEVER surface it. Mirrors the `emailConfigured ?? true` pattern. + get emailVerified() { return session?.user?.email_verified ?? true; }, get loading() { return loading; }, async load() { diff --git a/web/src/routes/[username]/[workspace]/+layout.svelte b/web/src/routes/[username]/[workspace]/+layout.svelte index 4cef4787..6b31ea33 100644 --- a/web/src/routes/[username]/[workspace]/+layout.svelte +++ b/web/src/routes/[username]/[workspace]/+layout.svelte @@ -13,6 +13,7 @@ import { authStore } from '$lib/stores/auth.svelte'; import { registerWorkspaceTools, type WebMcpHandle } from '$lib/webmcp/register'; import ConnectBanner from '$lib/components/ConnectBanner.svelte'; + import VerifyEmailBanner from '$lib/components/VerifyEmailBanner.svelte'; import BottomNav from '$lib/components/layout/BottomNav.svelte'; import MobileContextBar from '$lib/components/layout/MobileContextBar.svelte'; @@ -237,6 +238,8 @@ } ++ We sent a verification link to {registeredEmail}. + Confirm your email to start creating and sharing. +
++ You can look around now, but you'll need to verify before you can create or share. +
+ {#if resendState === 'sent'} +Verification email sent.
+ {:else} + + {/if} + +Create your account
@@ -441,4 +517,43 @@ .username-status.taken { color: #ef4444; } + + .verify-notice { + display: flex; + flex-direction: column; + gap: var(--space-4); + text-align: left; + } + + .verify-notice h2 { + font-size: 1.2rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; + text-align: center; + } + + .verify-notice p { + color: var(--text-secondary); + font-size: 0.9rem; + line-height: 1.5; + margin: 0; + } + + .verify-hint { + color: var(--text-muted) !important; + font-size: 0.82rem !important; + } + + .verify-sent { + color: #22c55e !important; + font-weight: 500; + text-align: center; + } + + .verify-notice button.secondary { + background: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border); + }