mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
SessionProvider: centralised state management
this replaces auth-provider, centralising user data can be extended to keep additional data allows for user data to propogate components throughout the app provides useSession and useAuthenticatedSession()
This commit is contained in:
@@ -1,44 +0,0 @@
|
|||||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
|
||||||
import { Auth } from "@/components/auth-provider";
|
|
||||||
import NotFound from "@/pages/NotFound";
|
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
|
||||||
import Index from "@/pages/Index";
|
|
||||||
import Landing from "@/pages/Landing";
|
|
||||||
import Login from "@/pages/Login";
|
|
||||||
import Test from "@/pages/Test";
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
|
||||||
<BrowserRouter>
|
|
||||||
<Routes>
|
|
||||||
{/* public routes */}
|
|
||||||
<Route path="/" element={<Landing />} />
|
|
||||||
<Route path="/login" element={<Login />} />
|
|
||||||
|
|
||||||
{/* authed routes */}
|
|
||||||
<Route
|
|
||||||
path="/app"
|
|
||||||
element={
|
|
||||||
<Auth>
|
|
||||||
<Index />
|
|
||||||
</Auth>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/test"
|
|
||||||
element={
|
|
||||||
<Auth>
|
|
||||||
<Test />
|
|
||||||
</Auth>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route path="*" element={<NotFound />} />
|
|
||||||
</Routes>
|
|
||||||
</BrowserRouter>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { UserRecord } from "@issue/shared";
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useAuthenticatedSession } from "@/components/session-provider";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { Field } from "@/components/ui/field";
|
import { Field } from "@/components/ui/field";
|
||||||
@@ -8,7 +8,9 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { UploadAvatar } from "@/components/upload-avatar";
|
import { UploadAvatar } from "@/components/upload-avatar";
|
||||||
import { user } from "@/lib/server";
|
import { user } from "@/lib/server";
|
||||||
|
|
||||||
function AccountDialog({ onUpdate, trigger }: { onUpdate?: () => void; trigger?: ReactNode }) {
|
function AccountDialog({ trigger }: { trigger?: ReactNode }) {
|
||||||
|
const { user: currentUser, setUser } = useAuthenticatedSession();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
@@ -16,24 +18,18 @@ function AccountDialog({ onUpdate, trigger }: { onUpdate?: () => void; trigger?:
|
|||||||
const [avatarURL, setAvatarUrl] = useState<string | null>(null);
|
const [avatarURL, setAvatarUrl] = useState<string | null>(null);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [submitAttempted, setSubmitAttempted] = useState(false);
|
const [submitAttempted, setSubmitAttempted] = useState(false);
|
||||||
const [userId, setUserId] = useState<number | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
||||||
const userStr = localStorage.getItem("user");
|
setName(currentUser.name);
|
||||||
if (userStr) {
|
setUsername(currentUser.username);
|
||||||
const user = JSON.parse(userStr) as UserRecord;
|
setAvatarUrl(currentUser.avatarURL || null);
|
||||||
setName(user.name);
|
|
||||||
setUsername(user.username);
|
|
||||||
setUserId(user.id);
|
|
||||||
setAvatarUrl(user.avatarURL || null);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPassword("");
|
setPassword("");
|
||||||
setError("");
|
setError("");
|
||||||
setSubmitAttempted(false);
|
setSubmitAttempted(false);
|
||||||
}, [open]);
|
}, [open, currentUser]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -43,21 +39,15 @@ function AccountDialog({ onUpdate, trigger }: { onUpdate?: () => void; trigger?:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
setError("User not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await user.update({
|
await user.update({
|
||||||
id: userId,
|
id: currentUser.id,
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
password: password.trim(),
|
password: password.trim(),
|
||||||
avatarURL,
|
avatarURL,
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
setError("");
|
setError("");
|
||||||
localStorage.setItem("user", JSON.stringify(data));
|
setUser(data);
|
||||||
setPassword("");
|
setPassword("");
|
||||||
onUpdate?.();
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
onError: (errorMessage) => {
|
onError: (errorMessage) => {
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
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 { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils";
|
|
||||||
|
|
||||||
export function Auth({ children }: { children: React.ReactNode }) {
|
|
||||||
const [loggedIn, setLoggedIn] = useState<boolean>();
|
|
||||||
const fetched = useRef(false);
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (fetched.current) return;
|
|
||||||
fetched.current = true;
|
|
||||||
|
|
||||||
fetch(`${getServerURL()}/auth/me`, {
|
|
||||||
credentials: "include",
|
|
||||||
})
|
|
||||||
.then(async (res) => {
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`auth check failed: ${res.status}`);
|
|
||||||
}
|
|
||||||
const data = (await res.json()) as { user: UserRecord; csrfToken: string };
|
|
||||||
setLoggedIn(true);
|
|
||||||
setCsrfToken(data.csrfToken);
|
|
||||||
localStorage.setItem("user", JSON.stringify(data.user));
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setLoggedIn(false);
|
|
||||||
clearAuth();
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (loggedIn) {
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loggedIn === false) {
|
|
||||||
const next = encodeURIComponent(location.pathname + location.search);
|
|
||||||
return <Navigate to={`/login?next=${next}`} replace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Loading message={"Checking authentication"} />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { UserRecord } from "@issue/shared";
|
import type { UserRecord } from "@issue/shared";
|
||||||
import { type FormEvent, useState } from "react";
|
import { type FormEvent, useState } from "react";
|
||||||
|
import { useAuthenticatedSession } from "@/components/session-provider";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -26,7 +27,7 @@ export function CreateIssue({
|
|||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
completeAction?: (issueId: number) => void | Promise<void>;
|
completeAction?: (issueId: number) => void | Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const userId = JSON.parse(localStorage.getItem("user") || "{}").id as number | undefined;
|
const { user } = useAuthenticatedSession();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
@@ -61,7 +62,7 @@ export function CreateIssue({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userId) {
|
if (!user.id) {
|
||||||
setError("you must be logged in to create an issue");
|
setError("you must be logged in to create an issue");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { type FormEvent, useState } from "react";
|
import { type FormEvent, useState } from "react";
|
||||||
|
import { useAuthenticatedSession } from "@/components/session-provider";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -28,7 +29,7 @@ export function CreateOrganisation({
|
|||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
completeAction?: (organisationId: number) => void | Promise<void>;
|
completeAction?: (organisationId: number) => void | Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const userId = JSON.parse(localStorage.getItem("user") || "{}").id as number | undefined;
|
const { user } = useAuthenticatedSession();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
@@ -64,7 +65,7 @@ export function CreateOrganisation({
|
|||||||
if (name.trim() === "" || name.trim().length > 16) return;
|
if (name.trim() === "" || name.trim().length > 16) return;
|
||||||
if (slug.trim() === "" || slug.trim().length > 16) return;
|
if (slug.trim() === "" || slug.trim().length > 16) return;
|
||||||
|
|
||||||
if (!userId) {
|
if (!user.id) {
|
||||||
setError("you must be logged in to create an organisation");
|
setError("you must be logged in to create an organisation");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -75,7 +76,7 @@ export function CreateOrganisation({
|
|||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
description,
|
description,
|
||||||
userId,
|
userId: user.id,
|
||||||
onSuccess: async (data) => {
|
onSuccess: async (data) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
reset();
|
reset();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ProjectRecord } from "@issue/shared";
|
import type { ProjectRecord } from "@issue/shared";
|
||||||
import { type FormEvent, useState } from "react";
|
import { type FormEvent, useState } from "react";
|
||||||
|
import { useAuthenticatedSession } from "@/components/session-provider";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -29,7 +30,7 @@ export function CreateProject({
|
|||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
completeAction?: (projectId: number) => void | Promise<void>;
|
completeAction?: (projectId: number) => void | Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const userId = JSON.parse(localStorage.getItem("user") || "{}").id as number | undefined;
|
const { user } = useAuthenticatedSession();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
@@ -64,7 +65,7 @@ export function CreateProject({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userId) {
|
if (!user.id) {
|
||||||
setError("you must be logged in to create a project");
|
setError("you must be logged in to create a project");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -79,7 +80,7 @@ export function CreateProject({
|
|||||||
await project.create({
|
await project.create({
|
||||||
key,
|
key,
|
||||||
name,
|
name,
|
||||||
creatorId: userId,
|
creatorId: user.id,
|
||||||
organisationId,
|
organisationId,
|
||||||
onSuccess: async (data) => {
|
onSuccess: async (data) => {
|
||||||
const project = data as ProjectRecord;
|
const project = data as ProjectRecord;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import Avatar from "@/components/avatar";
|
import Avatar from "@/components/avatar";
|
||||||
import { ServerConfigurationDialog } from "@/components/server-configuration-dialog";
|
import { ServerConfigurationDialog } from "@/components/server-configuration-dialog";
|
||||||
|
import { useSession } from "@/components/session-provider";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { Field } from "@/components/ui/field";
|
import { Field } from "@/components/ui/field";
|
||||||
@@ -20,6 +21,7 @@ const DEMO_USERS = [
|
|||||||
export default function LogInForm() {
|
export default function LogInForm() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
const { setUser } = useSession();
|
||||||
|
|
||||||
const [loginDetailsOpen, setLoginDetailsOpen] = useState(false);
|
const [loginDetailsOpen, setLoginDetailsOpen] = useState(false);
|
||||||
const [showWarning, setShowWarning] = useState(() => {
|
const [showWarning, setShowWarning] = useState(() => {
|
||||||
@@ -51,7 +53,7 @@ export default function LogInForm() {
|
|||||||
setError("");
|
setError("");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setCsrfToken(data.csrfToken);
|
setCsrfToken(data.csrfToken);
|
||||||
localStorage.setItem("user", JSON.stringify(data.user));
|
setUser(data.user);
|
||||||
const next = searchParams.get("next") || "/app";
|
const next = searchParams.get("next") || "/app";
|
||||||
navigate(next, { replace: true });
|
navigate(next, { replace: true });
|
||||||
}
|
}
|
||||||
@@ -89,7 +91,7 @@ export default function LogInForm() {
|
|||||||
setError("");
|
setError("");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setCsrfToken(data.csrfToken);
|
setCsrfToken(data.csrfToken);
|
||||||
localStorage.setItem("user", JSON.stringify(data.user));
|
setUser(data.user);
|
||||||
const next = searchParams.get("next") || "/app";
|
const next = searchParams.get("next") || "/app";
|
||||||
navigate(next, { replace: true });
|
navigate(next, { replace: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { OrganisationMemberResponse, OrganisationResponse, UserRecord } from "@issue/shared";
|
import type { OrganisationMemberResponse, OrganisationResponse } from "@issue/shared";
|
||||||
import { Plus, X } from "lucide-react";
|
import { Plus, X } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { AddMemberDialog } from "@/components/add-member-dialog";
|
import { AddMemberDialog } from "@/components/add-member-dialog";
|
||||||
import { OrganisationSelect } from "@/components/organisation-select";
|
import { OrganisationSelect } from "@/components/organisation-select";
|
||||||
|
import { useAuthenticatedSession } from "@/components/session-provider";
|
||||||
import SmallUserDisplay from "@/components/small-user-display";
|
import SmallUserDisplay from "@/components/small-user-display";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||||
@@ -23,7 +24,7 @@ function OrganisationsDialog({
|
|||||||
setSelectedOrganisation: (organisation: OrganisationResponse | null) => void;
|
setSelectedOrganisation: (organisation: OrganisationResponse | null) => void;
|
||||||
refetchOrganisations: (options?: { selectOrganisationId?: number }) => Promise<void>;
|
refetchOrganisations: (options?: { selectOrganisationId?: number }) => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const user = JSON.parse(localStorage.getItem("user") || "{}") as UserRecord;
|
const { user } = useAuthenticatedSession();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [members, setMembers] = useState<OrganisationMemberResponse[]>([]);
|
const [members, setMembers] = useState<OrganisationMemberResponse[]>([]);
|
||||||
|
|||||||
84
packages/frontend/src/components/session-provider.tsx
Normal file
84
packages/frontend/src/components/session-provider.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import type { UserRecord } from "@issue/shared";
|
||||||
|
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import { Navigate, useLocation } from "react-router-dom";
|
||||||
|
import Loading from "@/components/loading";
|
||||||
|
import { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface SessionContextValue {
|
||||||
|
user: UserRecord | null;
|
||||||
|
setUser: (user: UserRecord) => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SessionContext = createContext<SessionContextValue | null>(null);
|
||||||
|
|
||||||
|
// for use outside RequireAuth
|
||||||
|
export function useSession(): SessionContextValue {
|
||||||
|
const context = useContext(SessionContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useSession must be used within a SessionProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// for use inside RequireAuth
|
||||||
|
export function useAuthenticatedSession(): { user: UserRecord; setUser: (user: UserRecord) => void } {
|
||||||
|
const { user, setUser } = useSession();
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("useAuthenticatedSession must be used within RequireAuth");
|
||||||
|
}
|
||||||
|
return { user, setUser };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [user, setUserState] = useState<UserRecord | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const fetched = useRef(false);
|
||||||
|
|
||||||
|
const setUser = useCallback((user: UserRecord) => {
|
||||||
|
setUserState(user);
|
||||||
|
localStorage.setItem("user", JSON.stringify(user));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fetched.current) return;
|
||||||
|
fetched.current = true;
|
||||||
|
|
||||||
|
fetch(`${getServerURL()}/auth/me`, {
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`auth check failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
const data = (await res.json()) as { user: UserRecord; csrfToken: string };
|
||||||
|
setUser(data.user);
|
||||||
|
setCsrfToken(data.csrfToken);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setUserState(null);
|
||||||
|
clearAuth();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, [setUser]);
|
||||||
|
|
||||||
|
return <SessionContext.Provider value={{ user, setUser, isLoading }}>{children}</SessionContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||||
|
const { user, isLoading } = useSession();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading message={"Checking authentication"} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
const next = encodeURIComponent(location.pathname + location.search);
|
||||||
|
return <Navigate to={`/login?next=${next}`} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
@@ -1,10 +1,47 @@
|
|||||||
import "./App.css";
|
import "./App.css";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "@/app";
|
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||||
|
import { RequireAuth, SessionProvider } from "@/components/session-provider";
|
||||||
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
import App from "@/pages/App";
|
||||||
|
import Landing from "@/pages/Landing";
|
||||||
|
import Login from "@/pages/Login";
|
||||||
|
import NotFound from "@/pages/NotFound";
|
||||||
|
import Test from "@/pages/Test";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||||
|
<BrowserRouter>
|
||||||
|
<SessionProvider>
|
||||||
|
<Routes>
|
||||||
|
{/* public routes */}
|
||||||
|
<Route path="/" element={<Landing />} />
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
|
||||||
|
{/* authed routes */}
|
||||||
|
<Route
|
||||||
|
path="/app"
|
||||||
|
element={
|
||||||
|
<RequireAuth>
|
||||||
|
<App />
|
||||||
|
</RequireAuth>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/test"
|
||||||
|
element={
|
||||||
|
<RequireAuth>
|
||||||
|
<Test />
|
||||||
|
</RequireAuth>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
|
</SessionProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</ThemeProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { OrganisationSelect } from "@/components/organisation-select";
|
|||||||
import OrganisationsDialog from "@/components/organisations-dialog";
|
import OrganisationsDialog from "@/components/organisations-dialog";
|
||||||
import { ProjectSelect } from "@/components/project-select";
|
import { ProjectSelect } from "@/components/project-select";
|
||||||
import { ServerConfigurationDialog } from "@/components/server-configuration-dialog";
|
import { ServerConfigurationDialog } from "@/components/server-configuration-dialog";
|
||||||
|
import { useAuthenticatedSession } from "@/components/session-provider";
|
||||||
import SmallUserDisplay from "@/components/small-user-display";
|
import SmallUserDisplay from "@/components/small-user-display";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -30,10 +31,8 @@ import { issue, organisation, project } from "@/lib/server";
|
|||||||
|
|
||||||
const BREATHING_ROOM = 1;
|
const BREATHING_ROOM = 1;
|
||||||
|
|
||||||
function Index() {
|
export default function App() {
|
||||||
const userData = JSON.parse(localStorage.getItem("user") || "{}") as UserRecord;
|
const { user } = useAuthenticatedSession();
|
||||||
|
|
||||||
const [user, setUser] = useState<UserRecord>(userData);
|
|
||||||
|
|
||||||
const organisationsRef = useRef(false);
|
const organisationsRef = useRef(false);
|
||||||
const [organisations, setOrganisations] = useState<OrganisationResponse[]>([]);
|
const [organisations, setOrganisations] = useState<OrganisationResponse[]>([]);
|
||||||
@@ -47,11 +46,6 @@ function Index() {
|
|||||||
|
|
||||||
const [members, setMembers] = useState<UserRecord[]>([]);
|
const [members, setMembers] = useState<UserRecord[]>([]);
|
||||||
|
|
||||||
const refetchUser = async () => {
|
|
||||||
const userData = JSON.parse(localStorage.getItem("user") || "{}") as UserRecord;
|
|
||||||
setUser(userData);
|
|
||||||
};
|
|
||||||
|
|
||||||
const refetchOrganisations = async (options?: { selectOrganisationId?: number }) => {
|
const refetchOrganisations = async (options?: { selectOrganisationId?: number }) => {
|
||||||
try {
|
try {
|
||||||
await organisation.byUser({
|
await organisation.byUser({
|
||||||
@@ -260,11 +254,7 @@ function Index() {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align={"end"}>
|
<DropdownMenuContent align={"end"}>
|
||||||
<DropdownMenuItem asChild className="flex items-end justify-end">
|
<DropdownMenuItem asChild className="flex items-end justify-end">
|
||||||
<AccountDialog
|
<AccountDialog />
|
||||||
onUpdate={async () => {
|
|
||||||
refetchUser();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild className="flex items-end justify-end">
|
<DropdownMenuItem asChild className="flex items-end justify-end">
|
||||||
<OrganisationsDialog
|
<OrganisationsDialog
|
||||||
@@ -334,5 +324,3 @@ function Index() {
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Index;
|
|
||||||
@@ -1,45 +1,16 @@
|
|||||||
import type { UserRecord } from "@issue/shared";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { useSession } from "@/components/session-provider";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils";
|
|
||||||
|
|
||||||
type AuthState = "unknown" | "authenticated" | "unauthenticated";
|
|
||||||
|
|
||||||
export default function Landing() {
|
export default function Landing() {
|
||||||
const [authState, setAuthState] = useState<AuthState>("unknown");
|
const { user, isLoading } = useSession();
|
||||||
const verifiedRef = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (verifiedRef.current) return;
|
|
||||||
verifiedRef.current = true;
|
|
||||||
|
|
||||||
fetch(`${getServerURL()}/auth/me`, {
|
|
||||||
credentials: "include",
|
|
||||||
})
|
|
||||||
.then(async (res) => {
|
|
||||||
if (res.ok) {
|
|
||||||
const data = (await res.json()) as { user: UserRecord; csrfToken: string };
|
|
||||||
localStorage.setItem("user", JSON.stringify(data.user));
|
|
||||||
setCsrfToken(data.csrfToken);
|
|
||||||
setAuthState("authenticated");
|
|
||||||
} else {
|
|
||||||
clearAuth();
|
|
||||||
setAuthState("unauthenticated");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
clearAuth();
|
|
||||||
setAuthState("unauthenticated");
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
<header className="relative flex items-center justify-center p-2 border-b">
|
<header className="relative flex items-center justify-center p-2 border-b">
|
||||||
<div className="text-3xl font-basteleur font-700">Issue</div>
|
<div className="text-3xl font-basteleur font-700">Issue</div>
|
||||||
<nav className="absolute right-2 flex items-center gap-4">
|
<nav className="absolute right-2 flex items-center gap-4">
|
||||||
{authState === "authenticated" ? (
|
{!isLoading && user ? (
|
||||||
<Button asChild variant="outline" size="sm">
|
<Button asChild variant="outline" size="sm">
|
||||||
<Link to="/app">Open app</Link>
|
<Link to="/app">Open app</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -65,7 +36,7 @@ export default function Landing() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{authState === "authenticated" ? (
|
{!isLoading && user ? (
|
||||||
<Button asChild size="lg">
|
<Button asChild size="lg">
|
||||||
<Link to="/app">Open app</Link>
|
<Link to="/app">Open app</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,44 +1,29 @@
|
|||||||
import type { UserRecord } from "@issue/shared";
|
import { useEffect } 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 { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils";
|
import { useSession } from "@/components/session-provider";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [checking, setChecking] = useState(true);
|
const { user, isLoading } = useSession();
|
||||||
const checkedRef = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (checkedRef.current) return;
|
if (!isLoading && user) {
|
||||||
checkedRef.current = true;
|
const next = searchParams.get("next") || "/app";
|
||||||
|
navigate(next, { replace: true });
|
||||||
|
}
|
||||||
|
}, [user, isLoading, navigate, searchParams]);
|
||||||
|
|
||||||
fetch(`${getServerURL()}/auth/me`, {
|
if (isLoading) {
|
||||||
credentials: "include",
|
|
||||||
})
|
|
||||||
.then(async (res) => {
|
|
||||||
if (res.ok) {
|
|
||||||
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 {
|
|
||||||
clearAuth();
|
|
||||||
setChecking(false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setChecking(false);
|
|
||||||
});
|
|
||||||
}, [navigate, searchParams]);
|
|
||||||
|
|
||||||
if (checking) {
|
|
||||||
return <Loading message="Checking authentication" />;
|
return <Loading message="Checking authentication" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
return <Loading message="Redirecting" />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center gap-4 w-full h-[100vh]">
|
<div className="flex flex-col items-center justify-center gap-4 w-full h-[100vh]">
|
||||||
<LogInForm />
|
<LogInForm />
|
||||||
|
|||||||
Reference in New Issue
Block a user