mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
migrated auth components from localStorage (token) to cookie-based auth
This commit is contained in:
@@ -2,7 +2,7 @@ import type { UserRecord } from "@issue/shared";
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Navigate, useLocation } from "react-router-dom";
|
import { Navigate, useLocation } from "react-router-dom";
|
||||||
import Loading from "@/components/loading";
|
import Loading from "@/components/loading";
|
||||||
import { getServerURL } from "@/lib/utils";
|
import { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils";
|
||||||
|
|
||||||
export function Auth({ children }: { children: React.ReactNode }) {
|
export function Auth({ children }: { children: React.ReactNode }) {
|
||||||
const [loggedIn, setLoggedIn] = useState<boolean>();
|
const [loggedIn, setLoggedIn] = useState<boolean>();
|
||||||
@@ -12,29 +12,22 @@ export function Auth({ children }: { children: React.ReactNode }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fetched.current) return;
|
if (fetched.current) return;
|
||||||
fetched.current = true;
|
fetched.current = true;
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
if (!token) {
|
|
||||||
return setLoggedIn(false);
|
|
||||||
}
|
|
||||||
fetch(`${getServerURL()}/auth/me`, {
|
fetch(`${getServerURL()}/auth/me`, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
credentials: "include",
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`auth check failed: ${res.status}`);
|
throw new Error(`auth check failed: ${res.status}`);
|
||||||
}
|
}
|
||||||
return (await res.json()) as UserRecord;
|
const data = (await res.json()) as { user: UserRecord; csrfToken: string };
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
setLoggedIn(true);
|
setLoggedIn(true);
|
||||||
localStorage.setItem("user", JSON.stringify(data));
|
setCsrfToken(data.csrfToken);
|
||||||
|
localStorage.setItem("user", JSON.stringify(data.user));
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLoggedIn(false);
|
setLoggedIn(false);
|
||||||
localStorage.removeItem("token");
|
clearAuth();
|
||||||
localStorage.removeItem("user");
|
|
||||||
localStorage.removeItem("selectedOrganisationId");
|
|
||||||
localStorage.removeItem("selectedProjectId");
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -2,45 +2,34 @@ import type { UserRecord } from "@issue/shared";
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { getServerURL } from "@/lib/utils";
|
import { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils";
|
||||||
|
|
||||||
type AuthState = "unknown" | "authenticated" | "unauthenticated";
|
type AuthState = "unknown" | "authenticated" | "unauthenticated";
|
||||||
|
|
||||||
export default function Landing() {
|
export default function Landing() {
|
||||||
const [authState, setAuthState] = useState<AuthState>(() => {
|
const [authState, setAuthState] = useState<AuthState>("unknown");
|
||||||
// if token exists, assume authenticated until verified
|
|
||||||
return localStorage.getItem("token") ? "unknown" : "unauthenticated";
|
|
||||||
});
|
|
||||||
const verifiedRef = useRef(false);
|
const verifiedRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (verifiedRef.current) return;
|
if (verifiedRef.current) return;
|
||||||
verifiedRef.current = true;
|
verifiedRef.current = true;
|
||||||
|
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
if (!token) {
|
|
||||||
setAuthState("unauthenticated");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify token in background
|
|
||||||
fetch(`${getServerURL()}/auth/me`, {
|
fetch(`${getServerURL()}/auth/me`, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
credentials: "include",
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const user = (await res.json()) as UserRecord;
|
const data = (await res.json()) as { user: UserRecord; csrfToken: string };
|
||||||
localStorage.setItem("user", JSON.stringify(user));
|
localStorage.setItem("user", JSON.stringify(data.user));
|
||||||
|
setCsrfToken(data.csrfToken);
|
||||||
setAuthState("authenticated");
|
setAuthState("authenticated");
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem("token");
|
clearAuth();
|
||||||
localStorage.removeItem("user");
|
|
||||||
setAuthState("unauthenticated");
|
setAuthState("unauthenticated");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
localStorage.removeItem("token");
|
clearAuth();
|
||||||
localStorage.removeItem("user");
|
|
||||||
setAuthState("unauthenticated");
|
setAuthState("unauthenticated");
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LogOut } from "lucide-react";
|
import { LogOut } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { clearAuth, cn, getCsrfToken, getServerURL } from "@/lib/utils";
|
||||||
|
|
||||||
export default function LogOutButton({
|
export default function LogOutButton({
|
||||||
noStyle = false,
|
noStyle = false,
|
||||||
@@ -12,12 +12,20 @@ export default function LogOutButton({
|
|||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const logOut = () => {
|
const logOut = async () => {
|
||||||
localStorage.removeItem("token");
|
const csrfToken = getCsrfToken();
|
||||||
localStorage.removeItem("user");
|
const headers: HeadersInit = {};
|
||||||
localStorage.removeItem("selectedOrganisationId");
|
if (csrfToken) headers["X-CSRF-Token"] = csrfToken;
|
||||||
localStorage.removeItem("selectedProjectId");
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(`${getServerURL()}/auth/logout`, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
clearAuth();
|
||||||
navigate(0);
|
navigate(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
|||||||
import { Field } from "@/components/ui/field";
|
import { Field } from "@/components/ui/field";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { UploadAvatar } from "@/components/upload-avatar";
|
import { UploadAvatar } from "@/components/upload-avatar";
|
||||||
import { capitalise, cn, getServerURL } from "@/lib/utils";
|
import { capitalise, cn, getServerURL, setCsrfToken } from "@/lib/utils";
|
||||||
|
|
||||||
const DEMO_USERS = [
|
const DEMO_USERS = [
|
||||||
{ name: "User 1", username: "u1", password: "a" },
|
{ name: "User 1", username: "u1", password: "a" },
|
||||||
@@ -43,14 +43,14 @@ export default function LogInForm() {
|
|||||||
fetch(`${getServerURL()}/auth/login`, {
|
fetch(`${getServerURL()}/auth/login`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|
||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ username, password }),
|
||||||
|
credentials: "include",
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
setError("");
|
setError("");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
localStorage.setItem("token", data.token);
|
setCsrfToken(data.csrfToken);
|
||||||
localStorage.setItem("user", JSON.stringify(data.user));
|
localStorage.setItem("user", JSON.stringify(data.user));
|
||||||
const next = searchParams.get("next") || "/app";
|
const next = searchParams.get("next") || "/app";
|
||||||
navigate(next, { replace: true });
|
navigate(next, { replace: true });
|
||||||
@@ -82,12 +82,13 @@ export default function LogInForm() {
|
|||||||
password,
|
password,
|
||||||
avatarURL,
|
avatarURL,
|
||||||
}),
|
}),
|
||||||
|
credentials: "include",
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
setError("");
|
setError("");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
localStorage.setItem("token", data.token);
|
setCsrfToken(data.csrfToken);
|
||||||
localStorage.setItem("user", JSON.stringify(data.user));
|
localStorage.setItem("user", JSON.stringify(data.user));
|
||||||
const next = searchParams.get("next") || "/app";
|
const next = searchParams.get("next") || "/app";
|
||||||
navigate(next, { replace: true });
|
navigate(next, { replace: true });
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import type { UserRecord } from "@issue/shared";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import Loading from "@/components/loading";
|
import Loading from "@/components/loading";
|
||||||
import LogInForm from "@/components/login-form";
|
import LogInForm from "@/components/login-form";
|
||||||
import { getServerURL } from "@/lib/utils";
|
import { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -14,24 +15,18 @@ export default function LoginPage() {
|
|||||||
if (checkedRef.current) return;
|
if (checkedRef.current) return;
|
||||||
checkedRef.current = true;
|
checkedRef.current = true;
|
||||||
|
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
if (!token) {
|
|
||||||
setChecking(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(`${getServerURL()}/auth/me`, {
|
fetch(`${getServerURL()}/auth/me`, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
credentials: "include",
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
// logged in, redirect to next if defined
|
const data = (await res.json()) as { user: UserRecord; csrfToken: string };
|
||||||
// fallback to /app
|
setCsrfToken(data.csrfToken);
|
||||||
|
localStorage.setItem("user", JSON.stringify(data.user));
|
||||||
const next = searchParams.get("next") || "/app";
|
const next = searchParams.get("next") || "/app";
|
||||||
navigate(next, { replace: true });
|
navigate(next, { replace: true });
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem("token");
|
clearAuth();
|
||||||
localStorage.removeItem("user");
|
|
||||||
setChecking(false);
|
setChecking(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user