diff --git a/docs/SETUP.md b/docs/SETUP.md index c5c5399d..ae9fd45d 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -215,6 +215,10 @@ In **Authentication → URL Configuration**: - `https://kalandra.tech/**` - `http://localhost:4321/**` (for local development) +#### Configure Auth Emails + +Sign-up confirmations and password-reset links are sent by Supabase itself, not the backend. Its built-in mailer only delivers to members of the Supabase project team, so set up a custom SMTP server under **Authentication → Emails → SMTP Settings** before real users can reset their passwords. + #### Create Storage Bucket 1. In Supabase dashboard: **Storage → New bucket** diff --git a/docs/frontend.md b/docs/frontend.md index d532daf9..1d175dae 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -82,6 +82,8 @@ Supabase Auth with email/password + Google OAuth. Client-side auth state is mana The sign-in dialog (`AuthDialog.astro`) is included in the layout — pages don't need to add it. +**Password reset.** The dialog's "Forgot password?" mode emails a Supabase recovery link. Like OAuth, the link lands on `/auth/callback` — the only page that accepts session tokens in the URL — which forwards to `/reset-password`. That page offers the new-password form only to sessions opened from an emailed link (`amr` of `otp` / `magiclink` / `recovery`), the same sessions Supabase lets set a password without the current one; a password or Google session changes its password on the profile page instead. + ## Blog Posts are first-class Astro pages, not a CMS — git is the source of truth. diff --git a/frontend/src/components/AuthDialog.astro b/frontend/src/components/AuthDialog.astro index a7524545..85120180 100644 --- a/frontend/src/components/AuthDialog.astro +++ b/frontend/src/components/AuthDialog.astro @@ -1,10 +1,10 @@ --- /** - * Sign-in / sign-up dialog with Turnstile protection and Google OAuth. + * Sign-in / sign-up / password-reset dialog with Turnstile protection and Google OAuth. * * Self-contained component: renders the markup and owns all the * dialog-specific JS (mode switching, Turnstile lifecycle, submit handlers). - * Exposes `window.__openAuthDialog()` so the header sign-in buttons (in + * Exposes `window.__openAuthDialog(mode?)` so the header sign-in buttons (in * Layout.astro) can trigger it without knowing about internals. * * Supabase client is a shared singleton from lib/supabase.ts so both this @@ -12,7 +12,7 @@ */ import { Icon } from "astro-icon/components"; import IconGoogle from "./icons/IconGoogle.astro"; -import type { Locale } from "../i18n/utils"; +import { localePath, type Locale } from "../i18n/utils"; import enCommon from "../i18n/en/common.json"; import csCommon from "../i18n/cs/common.json"; @@ -26,6 +26,8 @@ const t = lang === "cs" ? csCommon : enCommon; + @@ -92,22 +106,24 @@ const t = lang === "cs" ? csCommon : enCommon;

- -
-
- {t.auth.or} -
-
+
+ +
+
+ {t.auth.or} +
+
- - + + +
@@ -119,6 +135,10 @@ const t = lang === "cs" ? csCommon : enCommon; authHaveAccountLabel: t.auth.haveAccount, authConfirmationSentLabel: t.auth.confirmationSent, authVerifyingLabel: t.auth.verifying, + authResetPasswordLabel: t.auth.resetPassword, + authSendResetLinkLabel: t.auth.sendResetLink, + authRememberedPasswordLabel: t.auth.rememberedPassword, + authResetLinkSentLabel: t.auth.resetLinkSent, }} > window.__authDialogLabels = { @@ -128,10 +148,15 @@ const t = lang === "cs" ? csCommon : enCommon; haveAccount: authHaveAccountLabel, confirmationSent: authConfirmationSentLabel, verifying: authVerifyingLabel, + resetPassword: authResetPasswordLabel, + sendResetLink: authSendResetLinkLabel, + rememberedPassword: authRememberedPasswordLabel, + resetLinkSent: authResetLinkSentLabel, }; + diff --git a/frontend/src/pages/auth/callback.astro b/frontend/src/pages/auth/callback.astro index 4c42342c..610f9dda 100644 --- a/frontend/src/pages/auth/callback.astro +++ b/frontend/src/pages/auth/callback.astro @@ -1,10 +1,11 @@ --- /** - * OAuth callback landing page. + * Auth redirect landing page — for OAuth sign-ins and password-reset email links. * - * Supabase redirects here after a successful OAuth handshake with the URL hash - * containing `access_token` and friends. We load the Supabase client eagerly - * so `detectSessionInUrl` (default true) parses and persists the session + * Supabase redirects here after a successful OAuth handshake or a verified + * reset link, with the URL hash containing `access_token` and friends. We + * load the Supabase client eagerly so `detectSessionInUrl` (default true) + * parses and persists the session * before any other page-level auth UI tries to read from localStorage — this * avoids the "just signed in but header says signed out" flash that happened * when the redirect landed on a normal page and relied on the lazy-loaded @@ -13,7 +14,7 @@ * On success: redirect to `?next=` (or `/`) — the session is already in * localStorage so the destination's pre-paint script picks it up on first * render. - * On failure: forward to `/` with an `error_description` in the hash so the + * On failure: forward to `next` with an `error_description` in the hash so the * generic handler in Layout.astro surfaces the error snackbar. * * This page is intentionally bare (no Layout) so it renders in a single @@ -69,16 +70,13 @@ import enCommon from "../../i18n/en/common.json";