From 76cbcb7b03df34537556aab7c39d0b5ba5b60a01 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Fri, 9 Jan 2026 05:35:48 +0000 Subject: [PATCH] migrated auth components from localStorage (token) to cookie-based auth --- .../frontend/src/components/auth-provider.tsx | 21 +++++---------- packages/frontend/src/components/landing.tsx | 27 ++++++------------- .../src/components/log-out-button.tsx | 20 +++++++++----- .../frontend/src/components/login-form.tsx | 9 ++++--- .../frontend/src/components/login-page.tsx | 19 +++++-------- 5 files changed, 41 insertions(+), 55 deletions(-) diff --git a/packages/frontend/src/components/auth-provider.tsx b/packages/frontend/src/components/auth-provider.tsx index 8f34b69..5d350b4 100644 --- a/packages/frontend/src/components/auth-provider.tsx +++ b/packages/frontend/src/components/auth-provider.tsx @@ -2,7 +2,7 @@ import type { UserRecord } from "@issue/shared"; import { useEffect, useRef, useState } from "react"; import { Navigate, useLocation } from "react-router-dom"; import Loading from "@/components/loading"; -import { getServerURL } from "@/lib/utils"; +import { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils"; export function Auth({ children }: { children: React.ReactNode }) { const [loggedIn, setLoggedIn] = useState(); @@ -12,29 +12,22 @@ export function Auth({ children }: { children: React.ReactNode }) { useEffect(() => { if (fetched.current) return; fetched.current = true; - const token = localStorage.getItem("token"); - if (!token) { - return setLoggedIn(false); - } + fetch(`${getServerURL()}/auth/me`, { - headers: { Authorization: `Bearer ${token}` }, + credentials: "include", }) .then(async (res) => { if (!res.ok) { throw new Error(`auth check failed: ${res.status}`); } - return (await res.json()) as UserRecord; - }) - .then((data) => { + const data = (await res.json()) as { user: UserRecord; csrfToken: string }; setLoggedIn(true); - localStorage.setItem("user", JSON.stringify(data)); + setCsrfToken(data.csrfToken); + localStorage.setItem("user", JSON.stringify(data.user)); }) .catch(() => { setLoggedIn(false); - localStorage.removeItem("token"); - localStorage.removeItem("user"); - localStorage.removeItem("selectedOrganisationId"); - localStorage.removeItem("selectedProjectId"); + clearAuth(); }); }, []); diff --git a/packages/frontend/src/components/landing.tsx b/packages/frontend/src/components/landing.tsx index ff80c5f..1ce76b8 100644 --- a/packages/frontend/src/components/landing.tsx +++ b/packages/frontend/src/components/landing.tsx @@ -2,45 +2,34 @@ import type { UserRecord } from "@issue/shared"; import { useEffect, useRef, useState } from "react"; import { Link } from "react-router-dom"; import { Button } from "@/components/ui/button"; -import { getServerURL } from "@/lib/utils"; +import { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils"; type AuthState = "unknown" | "authenticated" | "unauthenticated"; export default function Landing() { - const [authState, setAuthState] = useState(() => { - // if token exists, assume authenticated until verified - return localStorage.getItem("token") ? "unknown" : "unauthenticated"; - }); + const [authState, setAuthState] = useState("unknown"); const verifiedRef = useRef(false); useEffect(() => { if (verifiedRef.current) return; verifiedRef.current = true; - const token = localStorage.getItem("token"); - if (!token) { - setAuthState("unauthenticated"); - return; - } - - // verify token in background fetch(`${getServerURL()}/auth/me`, { - headers: { Authorization: `Bearer ${token}` }, + credentials: "include", }) .then(async (res) => { if (res.ok) { - const user = (await res.json()) as UserRecord; - localStorage.setItem("user", JSON.stringify(user)); + const data = (await res.json()) as { user: UserRecord; csrfToken: string }; + localStorage.setItem("user", JSON.stringify(data.user)); + setCsrfToken(data.csrfToken); setAuthState("authenticated"); } else { - localStorage.removeItem("token"); - localStorage.removeItem("user"); + clearAuth(); setAuthState("unauthenticated"); } }) .catch(() => { - localStorage.removeItem("token"); - localStorage.removeItem("user"); + clearAuth(); setAuthState("unauthenticated"); }); }, []); diff --git a/packages/frontend/src/components/log-out-button.tsx b/packages/frontend/src/components/log-out-button.tsx index fd270bd..e7b3f9b 100644 --- a/packages/frontend/src/components/log-out-button.tsx +++ b/packages/frontend/src/components/log-out-button.tsx @@ -1,7 +1,7 @@ import { LogOut } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; +import { clearAuth, cn, getCsrfToken, getServerURL } from "@/lib/utils"; export default function LogOutButton({ noStyle = false, @@ -12,12 +12,20 @@ export default function LogOutButton({ }) { const navigate = useNavigate(); - const logOut = () => { - localStorage.removeItem("token"); - localStorage.removeItem("user"); - localStorage.removeItem("selectedOrganisationId"); - localStorage.removeItem("selectedProjectId"); + const logOut = async () => { + const csrfToken = getCsrfToken(); + const headers: HeadersInit = {}; + if (csrfToken) headers["X-CSRF-Token"] = csrfToken; + try { + await fetch(`${getServerURL()}/auth/logout`, { + method: "POST", + headers, + credentials: "include", + }); + } catch {} + + clearAuth(); navigate(0); }; diff --git a/packages/frontend/src/components/login-form.tsx b/packages/frontend/src/components/login-form.tsx index 53f9a5f..aa4106b 100644 --- a/packages/frontend/src/components/login-form.tsx +++ b/packages/frontend/src/components/login-form.tsx @@ -10,7 +10,7 @@ import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { Field } from "@/components/ui/field"; import { Label } from "@/components/ui/label"; import { UploadAvatar } from "@/components/upload-avatar"; -import { capitalise, cn, getServerURL } from "@/lib/utils"; +import { capitalise, cn, getServerURL, setCsrfToken } from "@/lib/utils"; const DEMO_USERS = [ { name: "User 1", username: "u1", password: "a" }, @@ -43,14 +43,14 @@ export default function LogInForm() { fetch(`${getServerURL()}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username, password }), + credentials: "include", }) .then(async (res) => { if (res.status === 200) { setError(""); const data = await res.json(); - localStorage.setItem("token", data.token); + setCsrfToken(data.csrfToken); localStorage.setItem("user", JSON.stringify(data.user)); const next = searchParams.get("next") || "/app"; navigate(next, { replace: true }); @@ -82,12 +82,13 @@ export default function LogInForm() { password, avatarURL, }), + credentials: "include", }) .then(async (res) => { if (res.status === 200) { setError(""); const data = await res.json(); - localStorage.setItem("token", data.token); + setCsrfToken(data.csrfToken); localStorage.setItem("user", JSON.stringify(data.user)); const next = searchParams.get("next") || "/app"; navigate(next, { replace: true }); diff --git a/packages/frontend/src/components/login-page.tsx b/packages/frontend/src/components/login-page.tsx index 671ee36..fdfa8a1 100644 --- a/packages/frontend/src/components/login-page.tsx +++ b/packages/frontend/src/components/login-page.tsx @@ -1,8 +1,9 @@ +import type { UserRecord } from "@issue/shared"; import { useEffect, useRef, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import Loading from "@/components/loading"; import LogInForm from "@/components/login-form"; -import { getServerURL } from "@/lib/utils"; +import { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils"; export default function LoginPage() { const navigate = useNavigate(); @@ -14,24 +15,18 @@ export default function LoginPage() { if (checkedRef.current) return; checkedRef.current = true; - const token = localStorage.getItem("token"); - if (!token) { - setChecking(false); - return; - } - fetch(`${getServerURL()}/auth/me`, { - headers: { Authorization: `Bearer ${token}` }, + credentials: "include", }) .then(async (res) => { if (res.ok) { - // logged in, redirect to next if defined - // fallback to /app + const data = (await res.json()) as { user: UserRecord; csrfToken: string }; + setCsrfToken(data.csrfToken); + localStorage.setItem("user", JSON.stringify(data.user)); const next = searchParams.get("next") || "/app"; navigate(next, { replace: true }); } else { - localStorage.removeItem("token"); - localStorage.removeItem("user"); + clearAuth(); setChecking(false); } })