Authorization working
This commit is contained in:
10
src/App.css
Normal file
10
src/App.css
Normal file
@@ -0,0 +1,10 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@theme {
|
||||
|
||||
}
|
||||
115
src/App.tsx
Normal file
115
src/App.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import "@/App.css";
|
||||
import { BrowserRouter, Route, Routes } from "react-router";
|
||||
import NavBar from "./components/nav/NavBar";
|
||||
import AdminPage from "./pages/protected/AdminPage";
|
||||
import GamePage from "./pages/protected/GamePage";
|
||||
import GamesPage from "./pages/protected/GamesPage";
|
||||
import LogoutPage from "./pages/protected/LogoutPage";
|
||||
import PersonPage from "./pages/protected/PersonPage";
|
||||
import RaidGroupPage from "./pages/protected/RaidGroupPage";
|
||||
import RaidGroupsPage from "./pages/protected/RaidGroupsPage";
|
||||
import RaidInstancePage from "./pages/protected/RaidInstancePage";
|
||||
import RaidLayoutPage from "./pages/protected/RaidLayoutPage";
|
||||
import HomePage from "./pages/public/HomePage";
|
||||
import LoginPage from "./pages/public/LoginPage";
|
||||
import SignupPage from "./pages/public/SignupPage";
|
||||
import { ProtectedRoute } from "./providers/AuthProvider";
|
||||
|
||||
|
||||
const publicRoutes: { path: string; element: React.ReactElement; }[] = [
|
||||
{
|
||||
path: "/",
|
||||
element: <HomePage/>
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
element: <LoginPage/>
|
||||
},
|
||||
{
|
||||
path: "/signup",
|
||||
element: <SignupPage/>
|
||||
}
|
||||
];
|
||||
|
||||
const protectedRoutes: { path: string; element: React.ReactElement; }[] = [
|
||||
{
|
||||
path: "/logout",
|
||||
element: <LogoutPage/>
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
element: <AdminPage/>
|
||||
},
|
||||
{
|
||||
path: "/game",
|
||||
element: <GamesPage/>
|
||||
},
|
||||
{
|
||||
path: "/game/:gameId",
|
||||
element: <GamePage/>
|
||||
},
|
||||
{
|
||||
path: "/raidGroup",
|
||||
element: <RaidGroupsPage/>
|
||||
},
|
||||
{
|
||||
path: "/raidGroup/:raidGroupId",
|
||||
element: <RaidGroupPage/>
|
||||
},
|
||||
{
|
||||
path: "/raidGroup/:raidGroupId/person/:personId",
|
||||
element: <PersonPage/>
|
||||
},
|
||||
{
|
||||
path: "/raidGroup/:raidGroupId/raidLayout/:raidLayoutId",
|
||||
element: <RaidLayoutPage/>
|
||||
},
|
||||
{
|
||||
path: "/raidGroup/:raidGroupId/raidInstance",
|
||||
element: <RaidInstancePage/>
|
||||
},
|
||||
{
|
||||
path: "/raidGroup/:raidGroupId/raidInstance/:raidInstanceId",
|
||||
element: <RaidInstancePage/>
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
export default function App(){
|
||||
return (
|
||||
<div
|
||||
className="mt-20"
|
||||
>
|
||||
<BrowserRouter>
|
||||
{/* Nav Bar */}
|
||||
<NavBar/>
|
||||
|
||||
{/* Routing */}
|
||||
<Routes>
|
||||
{/* Public Routes */}
|
||||
{
|
||||
publicRoutes.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{/* Protected Routes */}
|
||||
<Route element={<ProtectedRoute/>}>
|
||||
{
|
||||
protectedRoutes.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
src/components/button/PrimaryButton.tsx
Normal file
18
src/components/button/PrimaryButton.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import clsx from "clsx";
|
||||
import { ComponentProps } from "react";
|
||||
|
||||
|
||||
export default function PrimaryButton(props: ComponentProps<"button">){
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={clsx(
|
||||
props.className,
|
||||
"rounded-lg py-2 px-4",
|
||||
"bg-blue-500 dark:bg-blue-600 text-white"
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
63
src/components/input/PasswordInput.tsx
Normal file
63
src/components/input/PasswordInput.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import clsx from "clsx";
|
||||
import { ComponentProps } from "react";
|
||||
|
||||
|
||||
|
||||
interface PasswordInputProps extends ComponentProps<"input">{
|
||||
id: string;
|
||||
inputClasses?: string;
|
||||
labelClasses?: string;
|
||||
accepted?: boolean;
|
||||
}
|
||||
|
||||
|
||||
export default function PasswordInput(props: PasswordInputProps){
|
||||
const { id, placeholder, inputClasses, labelClasses, accepted } = props;
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row justify-center w-full rounded-sm bg-inherit"
|
||||
>
|
||||
<div
|
||||
className="relative bg-inherit w-full"
|
||||
>
|
||||
<input
|
||||
{...props}
|
||||
type="password"
|
||||
className={clsx(
|
||||
"peer bg-transparent w-full min-w-72 px-2 py-1 rounded-lg",
|
||||
"ring-2 focus:ring-sky-600 placeholder-transparent outline-hidden",
|
||||
inputClasses,
|
||||
{
|
||||
"ring-gray-500": accepted === undefined,
|
||||
"ring-red-600": accepted === false,
|
||||
"ring-green-600": accepted === true
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={id}
|
||||
id={`${id}Label`}
|
||||
className={clsx(
|
||||
"absolute cursor-text left-0 -top-3 mx-1 px-1",
|
||||
"bg-white dark:bg-neutral-825 text-sm",
|
||||
"peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-500 peer-placeholder-shown:top-1 peer-focus:-top-3 peer-focus:text-sky-600 peer-focus:text-sm",
|
||||
labelClasses,
|
||||
{
|
||||
"text-gray-500": accepted === undefined,
|
||||
"text-red-600": accepted === false,
|
||||
"text-green-600": accepted === true
|
||||
}
|
||||
)}
|
||||
style={{transitionProperty: "top, font-size, line-height",
|
||||
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
transitionDuration: "150ms"
|
||||
}}
|
||||
>
|
||||
{placeholder}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
64
src/components/input/TextInput.tsx
Normal file
64
src/components/input/TextInput.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import clsx from "clsx";
|
||||
import { ComponentProps } from "react";
|
||||
|
||||
|
||||
|
||||
interface TextInputProps extends ComponentProps<"input">{
|
||||
id: string;
|
||||
inputClasses?: string;
|
||||
labelClasses?: string;
|
||||
accepted?: boolean;
|
||||
}
|
||||
|
||||
|
||||
export default function TextInput(props: TextInputProps){
|
||||
const { id, placeholder, name, inputClasses, labelClasses, accepted } = props;
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row justify-center w-full rounded-sm bg-inherit"
|
||||
>
|
||||
<div
|
||||
className="relative bg-inherit w-full"
|
||||
>
|
||||
<input
|
||||
{...props}
|
||||
type="text"
|
||||
className={clsx(
|
||||
"peer bg-transparent w-full min-w-72 px-2 py-1 rounded-lg",
|
||||
"ring-2 focus:ring-sky-600 placeholder-transparent outline-hidden",
|
||||
inputClasses,
|
||||
{
|
||||
"ring-gray-500": accepted === undefined,
|
||||
"ring-red-600": accepted === false,
|
||||
"ring-green-600": accepted === true
|
||||
}
|
||||
)}
|
||||
name={name}
|
||||
/>
|
||||
<label
|
||||
htmlFor={id}
|
||||
id={`${id}Label`}
|
||||
className={clsx(
|
||||
"absolute cursor-text left-0 -top-3 mx-1 px-1",
|
||||
"bg-white dark:bg-neutral-825 text-sm",
|
||||
"peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-500 peer-placeholder-shown:top-1 peer-focus:-top-3 peer-focus:text-sky-600 peer-focus:text-sm",
|
||||
labelClasses,
|
||||
{
|
||||
"text-gray-500": accepted === undefined,
|
||||
"text-red-600": accepted === false,
|
||||
"text-green-600": accepted === true
|
||||
}
|
||||
)}
|
||||
style={{transitionProperty: "top, font-size, line-height",
|
||||
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
transitionDuration: "150ms"
|
||||
}}
|
||||
>
|
||||
{placeholder}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
src/components/nav/DarkModeToggle.tsx
Normal file
32
src/components/nav/DarkModeToggle.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { BsLightbulb, BsLightbulbFill } from "react-icons/bs";
|
||||
|
||||
|
||||
export default function DarkModeToggle(){
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
id="darkModeCheckbox"
|
||||
type="checkbox"
|
||||
className="peer hidden"
|
||||
onChange={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
defaultChecked={theme === "dark"}
|
||||
/>
|
||||
<label
|
||||
htmlFor="darkModeCheckbox"
|
||||
className="block peer-checked:hidden"
|
||||
>
|
||||
<BsLightbulbFill/>
|
||||
</label>
|
||||
<label
|
||||
htmlFor="darkModeCheckbox"
|
||||
className="hidden peer-checked:block"
|
||||
>
|
||||
<BsLightbulb/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
src/components/nav/NavBar.tsx
Normal file
56
src/components/nav/NavBar.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import clsx from "clsx";
|
||||
import { BsList } from "react-icons/bs";
|
||||
import { Link } from "react-router";
|
||||
import DarkModeToggle from "./DarkModeToggle";
|
||||
import ProtectedNavLinks from "./ProtectedNavLinks";
|
||||
import PublicNavLinks from "./PublicNavLinks";
|
||||
import raidBuilderIcon from "/raidBuilderIcon.svg";
|
||||
|
||||
|
||||
export default function NavBar(){
|
||||
return (
|
||||
<nav
|
||||
className={clsx(
|
||||
"fixed w-full top-0 left-0 border-b-2 z-40",
|
||||
"bg-gray-700 border-gray-600 dark:bg-zinc-900 dark:border-neutral-850 text-white"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="max-w-(--breakpoint-xl) flex flex-nowrap flex-row items-center justify-between mx-auto p-4"
|
||||
>
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center space-x-3 rtl:space-x-reverse"
|
||||
>
|
||||
<img
|
||||
src={raidBuilderIcon}
|
||||
alt="Raid Builder Logo"
|
||||
width={30}
|
||||
height={30}
|
||||
fetchPriority="high"
|
||||
/>
|
||||
<span
|
||||
className="self-center text-2xl font-semibold whitespace-nowrap"
|
||||
>
|
||||
Raid Builder
|
||||
</span>
|
||||
</Link>
|
||||
<div
|
||||
className="peer md:hidden text-3xl"
|
||||
>
|
||||
<BsList/>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"relative top-0 left-0 flex flex-row items-center rounded-lg space-x-4",
|
||||
"bg-gray-700 dark:bg-zinc-900"
|
||||
)}
|
||||
>
|
||||
<PublicNavLinks/>
|
||||
<ProtectedNavLinks/>
|
||||
<DarkModeToggle/>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
48
src/components/nav/ProtectedNavLinks.tsx
Normal file
48
src/components/nav/ProtectedNavLinks.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useAuth } from "@/providers/AuthProvider";
|
||||
import { NavLink } from "react-router";
|
||||
|
||||
|
||||
const protectedLinks = [
|
||||
{
|
||||
name: "Games",
|
||||
path: "/game"
|
||||
},
|
||||
{
|
||||
name: "Groups",
|
||||
path: "/raidGroup"
|
||||
},
|
||||
{
|
||||
name: "Admin",
|
||||
path: "/admin"
|
||||
},
|
||||
{
|
||||
name: "Logout",
|
||||
path: "/logout"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
export default function ProtectedNavLinks(){
|
||||
const { jwt } = useAuth();
|
||||
|
||||
|
||||
if(!jwt){
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
protectedLinks.map((link) => (
|
||||
<NavLink
|
||||
key={link.name}
|
||||
to={link.path}
|
||||
>
|
||||
{link.name}
|
||||
</NavLink>
|
||||
))
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
39
src/components/nav/PublicNavLinks.tsx
Normal file
39
src/components/nav/PublicNavLinks.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useAuth } from "@/providers/AuthProvider";
|
||||
import { NavLink } from "react-router";
|
||||
|
||||
|
||||
const publicLinks = [
|
||||
{
|
||||
name: "Home",
|
||||
path: "/"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
export default function PublicNavLinks(){
|
||||
const { jwt } = useAuth();
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
publicLinks.map((link) => (
|
||||
<NavLink
|
||||
key={link.name}
|
||||
to={link.path}
|
||||
>
|
||||
{link.name}
|
||||
</NavLink>
|
||||
))
|
||||
}
|
||||
{
|
||||
!jwt &&
|
||||
<NavLink
|
||||
to="/login"
|
||||
>
|
||||
Login
|
||||
</NavLink>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
3
src/hooks/AuthHooks.ts
Normal file
3
src/hooks/AuthHooks.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
|
||||
54
src/index.css
Normal file
54
src/index.css
Normal file
@@ -0,0 +1,54 @@
|
||||
@import "tailwindcss";
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--color-neutral-825: oklch(0.253 0 0);
|
||||
--color-neutral-850: oklch(0.237 0 0);
|
||||
}
|
||||
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: var(--color-neutral-825);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a.active {
|
||||
color: var(--color-blue-400);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
23
src/main.tsx
Normal file
23
src/main.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
import { AuthProvider } from './providers/AuthProvider.tsx'
|
||||
import { ThemeProvider } from './providers/ThemeProvider.tsx'
|
||||
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<ThemeProvider
|
||||
defaultTheme="dark"
|
||||
storageKey="vite-ui-theme"
|
||||
>
|
||||
<AuthProvider
|
||||
jwtStorageKey="jwt"
|
||||
refreshTokenStorageKey="refreshToken"
|
||||
>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
10
src/pages/protected/AdminPage.tsx
Normal file
10
src/pages/protected/AdminPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function AdminPage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Admin Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/pages/protected/GamePage.tsx
Normal file
10
src/pages/protected/GamePage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function GamePage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Game Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/pages/protected/GamesPage.tsx
Normal file
10
src/pages/protected/GamesPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function GamesPage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Games Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
src/pages/protected/LogoutPage.tsx
Normal file
32
src/pages/protected/LogoutPage.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useAuth } from "@/providers/AuthProvider";
|
||||
import { api } from "@/util/AxiosUtil";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
|
||||
export default function LogoutPage(){
|
||||
const { setJwt } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
const logout = async () => {
|
||||
const response = await api.get("/auth/logout");
|
||||
if(response.status === 200){
|
||||
setJwt(null);
|
||||
navigate("/");
|
||||
}
|
||||
else{
|
||||
//TODO: Handle error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={logout}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/pages/protected/PersonPage.tsx
Normal file
10
src/pages/protected/PersonPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function PersonPage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Person Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/pages/protected/RaidGroupPage.tsx
Normal file
10
src/pages/protected/RaidGroupPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function RaidGroupPage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Raid Group Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/pages/protected/RaidGroupsPage.tsx
Normal file
10
src/pages/protected/RaidGroupsPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function RaidGroupsPage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Raid Groups Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/pages/protected/RaidInstancePage.tsx
Normal file
10
src/pages/protected/RaidInstancePage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function RaidInstancePage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Raid Instance Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/pages/protected/RaidLayoutPage.tsx
Normal file
10
src/pages/protected/RaidLayoutPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function RaidLayoutPage(){
|
||||
//TODO:
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
Raid Layout Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
src/pages/public/HomePage.tsx
Normal file
7
src/pages/public/HomePage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function HomePage(){
|
||||
return (
|
||||
<main>
|
||||
Home Page
|
||||
</main>
|
||||
);
|
||||
}
|
||||
65
src/pages/public/LoginPage.tsx
Normal file
65
src/pages/public/LoginPage.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import PrimaryButton from "@/components/button/PrimaryButton";
|
||||
import PasswordInput from "@/components/input/PasswordInput";
|
||||
import TextInput from "@/components/input/TextInput";
|
||||
import { useAuth } from "@/providers/AuthProvider";
|
||||
import { Navigate, useNavigate } from "react-router";
|
||||
|
||||
|
||||
export default function LoginPage(){
|
||||
const { jwt, setJwt } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
const login = async (formData: FormData) => {
|
||||
const username = formData.get("username") as string;
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/token`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: "Basic " + btoa(`${username}:${password}`)
|
||||
},
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
if(response.status === 200){
|
||||
const json = await response.json();
|
||||
setJwt(json.token);
|
||||
navigate("/raidGroup");
|
||||
}
|
||||
else{
|
||||
//TODO: Handle error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(jwt){
|
||||
return <Navigate to="/"/>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<form
|
||||
action={login}
|
||||
className="flex flex-col justify-center space-y-8"
|
||||
>
|
||||
<TextInput
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
/>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
/>
|
||||
<PrimaryButton
|
||||
className="mx-auto"
|
||||
type="submit"
|
||||
>
|
||||
Login
|
||||
</PrimaryButton>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
7
src/pages/public/SignupPage.tsx
Normal file
7
src/pages/public/SignupPage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function SignupPage(){
|
||||
return (
|
||||
<div>
|
||||
Signup Page
|
||||
</div>
|
||||
);
|
||||
}
|
||||
112
src/providers/AuthProvider.tsx
Normal file
112
src/providers/AuthProvider.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { api } from "@/util/AxiosUtil";
|
||||
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useState } from "react";
|
||||
import { Navigate, Outlet } from "react-router";
|
||||
|
||||
|
||||
type AuthProviderProps = {
|
||||
children: React.ReactNode;
|
||||
jwtStorageKey?: string;
|
||||
refreshTokenStorageKey?: string;
|
||||
}
|
||||
|
||||
type AuthProviderState = {
|
||||
jwt: string | null;
|
||||
setJwt: (token: string | null) => void;
|
||||
expiration: Date | null;
|
||||
setExpiration: (expiration: Date | null) => void;
|
||||
}
|
||||
|
||||
const initialState: AuthProviderState = {
|
||||
jwt: null,
|
||||
setJwt: () => null,
|
||||
expiration: null,
|
||||
setExpiration: () => null
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthProviderState>(initialState);
|
||||
|
||||
|
||||
export function AuthProvider({
|
||||
children
|
||||
}: AuthProviderProps){
|
||||
const [ jwt, setJwt ] = useState<string | null>(null);
|
||||
const [ expiration, setExpiration ] = useState<Date | null>(null)
|
||||
|
||||
|
||||
const fetchToken = useCallback(async () => {
|
||||
try{
|
||||
const response = await api.get("/auth/refresh");
|
||||
//If the token is retrieved
|
||||
if((response.status === 200) && (!response.data.errors)){
|
||||
setJwt(response.data.token);
|
||||
setExpiration(new Date(atob(response.data.token.split(".")[1])));
|
||||
}
|
||||
//If the token cannot be retrieved
|
||||
else{
|
||||
setJwt(null);
|
||||
setExpiration(null);
|
||||
}
|
||||
}
|
||||
//If the token cannot be retrieved
|
||||
catch{
|
||||
setJwt(null);
|
||||
setExpiration(null);
|
||||
}
|
||||
}, [ setJwt, setExpiration ]);
|
||||
|
||||
|
||||
//Add the token to all queries
|
||||
useLayoutEffect(() => {
|
||||
if((expiration) && (expiration < new Date())){
|
||||
fetchToken();
|
||||
}
|
||||
|
||||
const authInterceptor = api.interceptors.request.use(config => {
|
||||
config.headers.Authorization = jwt ? `Bearer ${jwt}` : config.headers.Authorization;
|
||||
return config;
|
||||
});
|
||||
|
||||
return () => { api.interceptors.request.eject(authInterceptor); };
|
||||
}, [ jwt, expiration, fetchToken ]);
|
||||
|
||||
//Try to get the token on page load
|
||||
useEffect(() => {
|
||||
fetchToken();
|
||||
}, [ fetchToken ]);
|
||||
|
||||
|
||||
const currentTokens = useMemo(() => ({
|
||||
jwt,
|
||||
setJwt,
|
||||
expiration,
|
||||
setExpiration
|
||||
}), [ jwt, setJwt, expiration, setExpiration ]);
|
||||
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={currentTokens}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProtectedRoute(){
|
||||
const { jwt } = useAuth();
|
||||
|
||||
if(!jwt){
|
||||
return <Navigate to="/login"/>;
|
||||
}
|
||||
|
||||
return <Outlet/>;
|
||||
}
|
||||
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
|
||||
if(context === undefined){
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
70
src/providers/ThemeProvider.tsx
Normal file
70
src/providers/ThemeProvider.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
}
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null
|
||||
}
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vite-ui-theme"
|
||||
}: ThemeProviderProps){
|
||||
const [ theme, setTheme ] = useState<Theme>(localStorage.getItem(storageKey) as Theme || defaultTheme);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if(theme === "system"){
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
root.classList.add(systemTheme);
|
||||
}
|
||||
else{
|
||||
root.classList.add(theme);
|
||||
}
|
||||
}, [ theme ]);
|
||||
|
||||
|
||||
const currentTheme = useMemo(() => ({
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
}
|
||||
}), [ theme, setTheme, storageKey ]);
|
||||
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider value={currentTheme}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
|
||||
if(context === undefined){
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
8
src/util/AxiosUtil.ts
Normal file
8
src/util/AxiosUtil.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import axios from "axios";
|
||||
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: `${import.meta.env.VITE_API_URL}`,
|
||||
timeout: 10000,
|
||||
withCredentials: true
|
||||
});
|
||||
10
src/vite-env.d.ts
vendored
Normal file
10
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
Reference in New Issue
Block a user