Implement authentication with JWT
parent
97fee33264
commit
68149c1976
29
src/App.tsx
29
src/App.tsx
|
|
@ -5,23 +5,12 @@ import { Outlet, Route, Routes } from "react-router";
|
||||||
import { SignUpPage } from "./pages/signup/SignUpPage.tsx";
|
import { SignUpPage } from "./pages/signup/SignUpPage.tsx";
|
||||||
import { Register } from "./pages/Register.tsx";
|
import { Register } from "./pages/Register.tsx";
|
||||||
import { Buy } from "./pages/Buy.tsx";
|
import { Buy } from "./pages/Buy.tsx";
|
||||||
import { useState } from "react";
|
|
||||||
import { PreLoginPageContext } from "./context/pre-login-page-context.ts";
|
|
||||||
import { LoggedInPageContext } from "./context/logged-in-page-context.ts";
|
|
||||||
import { NotFound } from "./pages/NotFound.tsx";
|
import { NotFound } from "./pages/NotFound.tsx";
|
||||||
import { BuyReturn } from "./pages/BuyReturn.tsx";
|
import { BuyReturn } from "./pages/BuyReturn.tsx";
|
||||||
import { Grid, Paper } from "@mui/material";
|
import { Grid, Paper } from "@mui/material";
|
||||||
|
import { AuthenticatedRoute } from "./pages/auth/AuthenticatedRoute.tsx";
|
||||||
|
|
||||||
function App() {
|
const App = () => (
|
||||||
const [userId, setUserId] = useState<string | null>(
|
|
||||||
window.localStorage.getItem("paysplit-user-id"),
|
|
||||||
);
|
|
||||||
const storeUserId = (id: string) => {
|
|
||||||
window.localStorage.setItem("paysplit-user-id", id.toString());
|
|
||||||
setUserId(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper
|
<Paper
|
||||||
elevation={2}
|
elevation={2}
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -29,6 +18,7 @@ function App() {
|
||||||
bgcolor: "lightblue",
|
bgcolor: "lightblue",
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
maxWidth: "500px",
|
maxWidth: "500px",
|
||||||
|
minWidth: "350px",
|
||||||
width: "fit-content",
|
width: "fit-content",
|
||||||
margin: "auto",
|
margin: "auto",
|
||||||
}}
|
}}
|
||||||
|
|
@ -36,21 +26,13 @@ function App() {
|
||||||
<Grid container direction="column" alignContent="center" spacing={2}>
|
<Grid container direction="column" alignContent="center" spacing={2}>
|
||||||
<img className="app-logo" src={icon} alt="App logo" />
|
<img className="app-logo" src={icon} alt="App logo" />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
|
||||||
element={
|
|
||||||
<PreLoginPageContext.Provider value={{ setUserId: storeUserId }}>
|
|
||||||
<Outlet />
|
|
||||||
</PreLoginPageContext.Provider>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
</Route>
|
|
||||||
<Route
|
<Route
|
||||||
element={
|
element={
|
||||||
<LoggedInPageContext.Provider value={{ userId: userId! }}>
|
<AuthenticatedRoute>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</LoggedInPageContext.Provider>
|
</AuthenticatedRoute>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route path="/signup" element={<SignUpPage />} />
|
<Route path="/signup" element={<SignUpPage />} />
|
||||||
|
|
@ -62,6 +44,5 @@ function App() {
|
||||||
</Grid>
|
</Grid>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,49 @@
|
||||||
import axios, { type AxiosResponse } from "axios";
|
import axios, { type AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
const setJwtToken = (token: string) =>
|
||||||
|
window.localStorage.setItem("paysplit-jwt", token);
|
||||||
|
|
||||||
|
export const getJwtToken = () => window.localStorage.getItem("paysplit-jwt");
|
||||||
|
|
||||||
|
const getJwtHeader = () => {
|
||||||
|
const token = getJwtToken();
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("Unauthenticated");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
interface AccessResponse {
|
interface AccessResponse {
|
||||||
userId: string;
|
jwtToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const login = (name: string, password: string) =>
|
export const login = async (name: string, password: string) => {
|
||||||
axios.post("/access/login", {
|
const response = (await axios.post("/access/login", {
|
||||||
name,
|
name,
|
||||||
password,
|
password,
|
||||||
}) as Promise<AxiosResponse<AccessResponse, unknown, {}>>;
|
})) as AxiosResponse<AccessResponse>;
|
||||||
|
setJwtToken(response.data.jwtToken);
|
||||||
|
};
|
||||||
|
|
||||||
export const registerUser = (name: string, password: string) =>
|
export const registerUser = async (name: string, password: string) => {
|
||||||
axios.post("/access/register", {
|
const response = (await axios.post("/access/register", {
|
||||||
name,
|
name,
|
||||||
password,
|
password,
|
||||||
}) as Promise<AxiosResponse<AccessResponse, unknown, {}>>;
|
})) as AxiosResponse<AccessResponse>;
|
||||||
|
setJwtToken(response.data.jwtToken);
|
||||||
|
};
|
||||||
|
|
||||||
export const startBuyFlow = (userId: string, quantity: number) =>
|
export const startBuyFlow = (quantity: number) =>
|
||||||
axios.post("/buy/start", { userId, quantity }) as Promise<
|
axios.post("/buy/start", { quantity }, getJwtHeader()) as Promise<
|
||||||
AxiosResponse<{ url: string }, unknown, {}>
|
AxiosResponse<{ url: string }, unknown, {}>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const completeBuyFlow = (userId: string) =>
|
export const completeBuyFlow = () =>
|
||||||
axios.post("/buy/complete", { userId }) as Promise<
|
axios.post("/buy/complete", {}, getJwtHeader()) as Promise<
|
||||||
AxiosResponse<{ url: string }, unknown, {}>
|
AxiosResponse<{ url: string }, unknown, {}>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|
@ -35,21 +56,21 @@ export interface Session {
|
||||||
date: string;
|
date: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAllSessions = (userId: string) =>
|
export const getAllSessions = () =>
|
||||||
(
|
(
|
||||||
axios.get(`/session?userId=${userId}`) as Promise<AxiosResponse<Session[]>>
|
axios.get("/session", getJwtHeader()) as Promise<AxiosResponse<Session[]>>
|
||||||
).then((response) => response.data);
|
).then((response) => response.data);
|
||||||
|
|
||||||
export const joinSession = (userId: string, sessionId: string) =>
|
export const joinSession = (sessionId: string) =>
|
||||||
(
|
(
|
||||||
axios.post("/session/join", { userId, sessionId }) as Promise<
|
axios.post("/session/join", { sessionId }, getJwtHeader()) as Promise<
|
||||||
AxiosResponse<Session[]>
|
AxiosResponse<Session[]>
|
||||||
>
|
>
|
||||||
).then((response) => response.data);
|
).then((response) => response.data);
|
||||||
|
|
||||||
export const leaveSession = (userId: string, sessionId: string) =>
|
export const leaveSession = (sessionId: string) =>
|
||||||
(
|
(
|
||||||
axios.post("/session/leave", { userId, sessionId }) as Promise<
|
axios.post("/session/leave", { sessionId }, getJwtHeader()) as Promise<
|
||||||
AxiosResponse<Session[]>
|
AxiosResponse<Session[]>
|
||||||
>
|
>
|
||||||
).then((response) => response.data);
|
).then((response) => response.data);
|
||||||
|
|
@ -61,7 +82,7 @@ export interface Tokens {
|
||||||
used: number;
|
used: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAvailableTokenCount = (userId: string) =>
|
export const getAvailableTokenCount = () =>
|
||||||
(
|
(axios.get("/tokens", getJwtHeader()) as Promise<AxiosResponse<Tokens>>).then(
|
||||||
axios.get(`/tokens?userId=${userId}`) as Promise<AxiosResponse<Tokens>>
|
(response) => response.data,
|
||||||
).then((response) => response.data);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { createContext } from "react";
|
|
||||||
|
|
||||||
interface LoggedInPageContextProps {
|
|
||||||
userId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LoggedInPageContext =
|
|
||||||
createContext<LoggedInPageContextProps | null>(null);
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { createContext } from "react";
|
|
||||||
|
|
||||||
interface PreLoginPageContextProps {
|
|
||||||
setUserId: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PreLoginPageContext =
|
|
||||||
createContext<PreLoginPageContextProps | null>(null);
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useContext, useState } from "react";
|
import { useState } from "react";
|
||||||
import { startBuyFlow } from "../api/client";
|
import { startBuyFlow } from "../api/client";
|
||||||
import { LoggedInPageContext } from "../context/logged-in-page-context";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
|
|
@ -14,10 +13,9 @@ import { useNavigate } from "react-router";
|
||||||
|
|
||||||
export const Buy = () => {
|
export const Buy = () => {
|
||||||
const [quantity, setQuantity] = useState(0);
|
const [quantity, setQuantity] = useState(0);
|
||||||
const { userId } = useContext(LoggedInPageContext)!;
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const handleBuyFlow = () =>
|
const handleBuyFlow = () =>
|
||||||
startBuyFlow(userId, quantity).then(
|
startBuyFlow(quantity).then(
|
||||||
(response) => (window.location.href = response.data.url),
|
(response) => (window.location.href = response.data.url),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
import { useContext } from "react";
|
|
||||||
import { completeBuyFlow } from "../api/client";
|
import { completeBuyFlow } from "../api/client";
|
||||||
import { LoggedInPageContext } from "../context/logged-in-page-context";
|
|
||||||
import { CircularProgress, Grid, Typography } from "@mui/material";
|
import { CircularProgress, Grid, Typography } from "@mui/material";
|
||||||
|
|
||||||
export const BuyReturn = () => {
|
export const BuyReturn = () => {
|
||||||
const { userId } = useContext(LoggedInPageContext)!;
|
completeBuyFlow().then(
|
||||||
completeBuyFlow(userId).then(
|
|
||||||
(response) => (window.location.href = response.data.url),
|
(response) => (window.location.href = response.data.url),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useContext, useState } from "react";
|
import { useState } from "react";
|
||||||
import { login } from "../api/client";
|
import { login } from "../api/client";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { PreLoginPageContext } from "../context/pre-login-page-context";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
|
@ -15,13 +14,10 @@ export const Login = () => {
|
||||||
const [userName, setUserName] = useState("");
|
const [userName, setUserName] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [rememberMe, setRememberMe] = useState(false);
|
const [rememberMe, setRememberMe] = useState(false);
|
||||||
|
|
||||||
const { setUserId } = useContext(PreLoginPageContext)!;
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleLoginFlow = () =>
|
const handleLoginFlow = () =>
|
||||||
login(userName, password).then((response) => {
|
login(userName, password).then(() => {
|
||||||
setUserId(response.data.userId);
|
|
||||||
navigate("/signup");
|
navigate("/signup");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { useContext, useState } from "react";
|
import { useState } from "react";
|
||||||
import * as zod from "zod";
|
import * as zod from "zod";
|
||||||
import { registerUser } from "../api/client";
|
import { registerUser } from "../api/client";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { PreLoginPageContext } from "../context/pre-login-page-context";
|
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -16,7 +15,6 @@ export const Register = () => {
|
||||||
const [userName, setUserName] = useState("");
|
const [userName, setUserName] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [passwordConfirm, setPasswordConfirm] = useState("");
|
const [passwordConfirm, setPasswordConfirm] = useState("");
|
||||||
const { setUserId } = useContext(PreLoginPageContext)!;
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const PASSWORD_MAX_LENGTH = 64;
|
const PASSWORD_MAX_LENGTH = 64;
|
||||||
|
|
@ -51,8 +49,7 @@ export const Register = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRegisterFlow = () =>
|
const handleRegisterFlow = () =>
|
||||||
registerUser(userName, password).then((response) => {
|
registerUser(userName, password).then(() => {
|
||||||
setUserId(response.data.userId);
|
|
||||||
navigate("/signup");
|
navigate("/signup");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { getJwtToken } from "../../api/client";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
export const AuthenticatedRoute = (props: { children: React.ReactNode }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const authToken = getJwtToken();
|
||||||
|
|
||||||
|
if (!authToken) {
|
||||||
|
navigate("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.children;
|
||||||
|
};
|
||||||
|
|
@ -5,8 +5,6 @@ import {
|
||||||
type Session,
|
type Session,
|
||||||
type Tokens,
|
type Tokens,
|
||||||
} from "../../api/client";
|
} from "../../api/client";
|
||||||
import { LoggedInPageContext } from "../../context/logged-in-page-context";
|
|
||||||
import { useContext } from "react";
|
|
||||||
import { Async } from "react-async";
|
import { Async } from "react-async";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
|
@ -15,7 +13,6 @@ export const SessionAction = (props: {
|
||||||
tokensPromise: Promise<Tokens>;
|
tokensPromise: Promise<Tokens>;
|
||||||
}) => {
|
}) => {
|
||||||
const { session, tokensPromise } = props;
|
const { session, tokensPromise } = props;
|
||||||
const { userId } = useContext(LoggedInPageContext)!;
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (session.status !== "OPEN") {
|
if (session.status !== "OPEN") {
|
||||||
|
|
@ -38,7 +35,7 @@ export const SessionAction = (props: {
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ width: "100%" }}
|
sx={{ width: "100%" }}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await leaveSession(userId, session.id);
|
await leaveSession(session.id);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -83,7 +80,7 @@ export const SessionAction = (props: {
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ width: "100%" }}
|
sx={{ width: "100%" }}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await joinSession(userId, session.id);
|
await joinSession(session.id);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Alert, Grid, Paper, Typography } from "@mui/material";
|
import { Alert, Grid, Paper, Typography } from "@mui/material";
|
||||||
import { useContext, useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Async } from "react-async";
|
import { Async } from "react-async";
|
||||||
import {
|
import {
|
||||||
getAllSessions,
|
getAllSessions,
|
||||||
|
|
@ -7,14 +7,12 @@ import {
|
||||||
type Session,
|
type Session,
|
||||||
type Tokens,
|
type Tokens,
|
||||||
} from "../../api/client";
|
} from "../../api/client";
|
||||||
import { LoggedInPageContext } from "../../context/logged-in-page-context";
|
|
||||||
import { SessionPicker } from "./SessionPicker";
|
import { SessionPicker } from "./SessionPicker";
|
||||||
import { TokensSummary } from "./TokensSummary";
|
import { TokensSummary } from "./TokensSummary";
|
||||||
|
|
||||||
export const SignUpPage = () => {
|
export const SignUpPage = () => {
|
||||||
const { userId } = useContext(LoggedInPageContext)!;
|
const sessionsPromise = useMemo(getAllSessions, []);
|
||||||
const sessionsPromise = useMemo(() => getAllSessions(userId), []);
|
const tokensPromise = getAvailableTokenCount();
|
||||||
const tokensPromise = getAvailableTokenCount(userId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container direction="column" spacing={2} alignItems="center">
|
<Grid container direction="column" spacing={2} alignItems="center">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue