diff --git a/docs/core-checklist-status.md b/docs/core-checklist-status.md index d71f67f..bb26152 100644 --- a/docs/core-checklist-status.md +++ b/docs/core-checklist-status.md @@ -7,7 +7,7 @@ Legend: ## Modules -1. Account - `PARTIAL` (email auth, JWT, refresh, logout, reset; integration tests now cover resend-verification token replacement and full password-reset login flow; sessions exist, full UX still improving) +1. Account - `PARTIAL` (email auth, JWT, refresh, logout, reset; web now handles `/verify-email?token=...` links and shows auth-page feedback; integration tests cover resend-verification token replacement and full password-reset login flow; sessions exist, full UX still improving) 2. User Profile - `DONE` (username, name, avatar, bio, update) 3. User Status - `PARTIAL` (online/last seen/offline; web now formats `just now/today/yesterday/recently`, backend-side presence heuristics still limited) 4. Contacts - `PARTIAL` (list/search/add/remove/block/unblock; `add by email` flow covered by integration tests including `success/not found/blocked conflict`; web now surfaces specific add-by-email errors (`not found` vs `blocked`); UX moved to menu) diff --git a/web/src/api/auth.ts b/web/src/api/auth.ts index 0ee5d2e..333776c 100644 --- a/web/src/api/auth.ts +++ b/web/src/api/auth.ts @@ -5,6 +5,10 @@ export async function registerRequest(email: string, name: string, username: str await http.post("/auth/register", { email, name, username, password }); } +export async function verifyEmailRequest(token: string): Promise { + await http.post("/auth/verify-email", { token }); +} + export async function loginRequest(email: string, password: string, otpCode?: string, recoveryCode?: string): Promise { const { data } = await http.post("/auth/login", { email, diff --git a/web/src/app/App.tsx b/web/src/app/App.tsx index 7ca17e9..adc6f39 100644 --- a/web/src/app/App.tsx +++ b/web/src/app/App.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { joinByInvite } from "../api/chats"; +import { verifyEmailRequest } from "../api/auth"; import { ToastViewport } from "../components/ToastViewport"; import { AuthPage } from "../pages/AuthPage"; import { ChatsPage } from "../pages/ChatsPage"; @@ -8,6 +9,7 @@ import { useChatStore } from "../store/chatStore"; import { applyAppearancePreferences, getAppPreferences } from "../utils/preferences"; const PENDING_INVITE_TOKEN_KEY = "bm_pending_invite_token"; +const AUTH_NOTICE_KEY = "bm_auth_notice"; export function App() { const accessToken = useAuthStore((s) => s.accessToken); @@ -37,6 +39,23 @@ export function App() { }); }, [accessToken, loadMe, refresh, logout]); + useEffect(() => { + const verificationToken = extractEmailVerificationTokenFromLocation(); + if (!verificationToken) { + return; + } + void (async () => { + try { + await verifyEmailRequest(verificationToken); + window.localStorage.setItem(AUTH_NOTICE_KEY, "Email confirmed. You can now sign in."); + } catch { + window.localStorage.setItem(AUTH_NOTICE_KEY, "Email verification failed or token expired."); + } finally { + window.history.replaceState(null, "", "/"); + } + })(); + }, []); + useEffect(() => { const token = extractInviteTokenFromLocation(); if (!token) { @@ -90,3 +109,14 @@ function extractInviteTokenFromLocation(): string | null { const match = url.pathname.match(/^\/join\/([^/]+)$/i); return match?.[1]?.trim() || null; } + +function extractEmailVerificationTokenFromLocation(): string | null { + if (typeof window === "undefined") { + return null; + } + const url = new URL(window.location.href); + if (!/^\/verify-email\/?$/i.test(url.pathname)) { + return null; + } + return url.searchParams.get("token")?.trim() || null; +} diff --git a/web/src/components/AuthPanel.tsx b/web/src/components/AuthPanel.tsx index fa93161..95073c9 100644 --- a/web/src/components/AuthPanel.tsx +++ b/web/src/components/AuthPanel.tsx @@ -1,9 +1,10 @@ import axios from "axios"; -import { FormEvent, useState } from "react"; +import { FormEvent, useEffect, useState } from "react"; import { checkEmailStatus, registerRequest } from "../api/auth"; import { useAuthStore } from "../store/authStore"; type Step = "email" | "password" | "register" | "otp"; +const AUTH_NOTICE_KEY = "bm_auth_notice"; export function AuthPanel() { const login = useAuthStore((s) => s.login); @@ -20,6 +21,15 @@ export function AuthPanel() { const [error, setError] = useState(null); const [success, setSuccess] = useState(null); + useEffect(() => { + const notice = window.localStorage.getItem(AUTH_NOTICE_KEY); + if (!notice) { + return; + } + setSuccess(notice); + window.localStorage.removeItem(AUTH_NOTICE_KEY); + }, []); + async function onSubmit(event: FormEvent) { event.preventDefault(); setError(null);