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