[Frontend] Implement Login with JWT and axios

This commit is contained in:
Pacerino
2022-10-03 22:50:21 +02:00
parent e7557515f1
commit aaef8a4c97
7 changed files with 227 additions and 26 deletions

View File

@@ -11,6 +11,7 @@
"@types/node": "^16.11.59",
"@types/react": "^18.0.20",
"@types/react-dom": "^18.0.6",
"axios": "^0.27.2",
"flowbite": "^1.5.3",
"flowbite-react": "^0.1.11",
"joi": "^17.6.0",

View File

@@ -1,20 +1,137 @@
import Layout from "./components/Layout";
import {
Routes,
Route,
useNavigate,
useLocation,
Navigate,
} from "react-router-dom";
import React from "react";
import Layout from "./components/Layout"
import { Routes, Route } from "react-router-dom";
import { localAuthProvider } from "./auth";
import { TextInput, Button, /* Alert */ } from "flowbite-react";
import { useForm, SubmitHandler } from "react-hook-form";
// Pages
import HostsPage from "./pages/Hosts"
import HostsPage from "./pages/Hosts";
function App() {
return (
<>
<AuthProvider>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<HostsPage />} />
<Route element={<Layout />}>
<Route path="/login" element={<LoginPage />} />
<Route
path="/"
element={
<RequireAuth>
<HostsPage />
</RequireAuth>
}
/>
</Route>
</Routes>
</>
</AuthProvider>
);
}
interface AuthContextType {
user: any;
signin: (mail: string, password: string, callback: VoidFunction) => void;
signout: (callback: VoidFunction) => void;
}
let AuthContext = React.createContext<AuthContextType>(null!);
function AuthProvider({ children }: { children: React.ReactNode }) {
let [user, setUser] = React.useState<any>(null);
let signin = (email: string, password: string, callback: VoidFunction) => {
return localAuthProvider.signin(email, password, () => {
setUser(email);
callback();
});
};
let signout = (callback: VoidFunction) => {
return localAuthProvider.signout(() => {
setUser(null);
callback();
});
};
let value = { user, signin, signout };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
return React.useContext(AuthContext);
}
function RequireAuth({ children }: { children: JSX.Element }) {
let auth = useAuth();
let location = useLocation();
if (!auth.user) {
// Redirect them to the /login page, but save the current location they were
// trying to go to when they were redirected. This allows us to send them
// along to that page after they login, which is a nicer user experience
// than dropping them off on the home page.
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
interface FormValues {
email: string;
password: string;
}
function LoginPage() {
let navigate = useNavigate();
let location = useLocation();
let from = location.state?.from?.pathname || "/";
let auth = useAuth();
const { register, handleSubmit } = useForm<FormValues>();
const performLogin: SubmitHandler<FormValues> = async (data) => {
auth.signin(data.email, data.password, () => {
navigate(from, { replace: true });
})
};
return (
<form onSubmit={handleSubmit(performLogin)}>
<div className="flex justify-center">
<div className="px-8 pt-6 pb-8 mb-4 flex flex-col w-1/4">
<h1 className="text-white text-4xl pb-8">Please login</h1>
{/* {error && (
<div className="pb-8">
<Alert color="failure">{error}</Alert>
</div>
)} */}
<div className="mb-4">
<TextInput
type="email"
placeholder="name@caddyproxymanager.com"
required={true}
{...register("email", { required: true })}
/>
</div>
<div className="mb-6">
<TextInput
type="password"
required={true}
{...register("password", { required: true })}
/>
</div>
<div className="flex items-center justify-between">
<Button type="submit">Login</Button>
</div>
</div>
</div>
</form>
);
}

23
frontend/src/auth.ts Normal file
View File

@@ -0,0 +1,23 @@
import http, { RequestResponse} from "./utils/axios"
const localAuthProvider = {
isAuthenticated: false,
signin(email: string, password: string, callback: VoidFunction) {
http
.post<RequestResponse>("/users/login", {
email: email,
secret: password,
})
.then(result => {
localStorage.setItem("token", result.data.result.token);
callback();
})
},
signout(callback: VoidFunction) {
localAuthProvider.isAuthenticated = false;
localStorage.setItem("token", "");
callback();
},
};
export { localAuthProvider };

View File

@@ -1,6 +1,10 @@
import { Navbar } from "flowbite-react";
import { Outlet } from "react-router-dom";
import { Navbar, Button } from "flowbite-react";
import { Outlet, useNavigate } from "react-router-dom";
import { useAuth } from "../App";
function Layout() {
const auth = useAuth();
const navigate = useNavigate();
return (
<div>
<Navbar fluid={true} rounded={false}>
@@ -14,8 +18,18 @@ function Layout() {
Caddy Proxy Manager
</span>
</Navbar.Brand>
{auth.user && (
<div className="flex md:order-2">
<Button onClick={() => auth.signout(() => navigate("/"))}>
Logout
</Button>
<Navbar.Toggle />
</div>
)}
<Navbar.Collapse>
{/* <Navbar.Link href="/home">Home</Navbar.Link>
{/* <Navbar.Link href="/home">Home</Navbar.Link>
<Navbar.Link href="/hosts">Hosts</Navbar.Link> */}
</Navbar.Collapse>
</Navbar>

View File

@@ -11,6 +11,7 @@ import { joiResolver } from "@hookform/resolvers/joi";
import Joi from "joi";
import { HiAdjustments, HiTrash, HiDocumentAdd } from "react-icons/hi";
import React from "react";
import http, { RequestResponse } from "../utils/axios";
type Domain = {
fqdn: string;
@@ -104,10 +105,9 @@ function HostsPage() {
}, []);
const getHosts = async () => {
return await fetch(`http://${window.location.hostname}:3001/api/hosts`)
.then((res) => res.json())
.then((json) => {
setHostData(json);
return await http.get<RequestResponse>(`/hosts`)
.then((res) => {
setHostData(res.data);
});
};
@@ -122,22 +122,14 @@ function HostsPage() {
upstreams: data.upstreams,
};
setLoading(true);
const res = await fetch(`http://${window.location.hostname}:3001/api/hosts`, {
body: JSON.stringify(jsonBody),
method: "POST",
});
if (res.status !== 200) {
// handle Error
}
await http.post(`hosts`, jsonBody);
setLoading(false);
setModal(false);
getHosts();
};
const deleteHost = async (hostID: number) => {
await fetch(`http://${window.location.hostname}:3001/api/hosts/${hostID}`, {
method: "DELETE",
});
await http.delete(`/hosts/${hostID}`);
getHosts();
};

View File

@@ -0,0 +1,37 @@
import axios, { AxiosRequestConfig } from "axios";
export interface RequestResponse {
result: any;
error: Error;
}
interface Error {
code: number;
message: string;
}
axios.interceptors.request.use((config: AxiosRequestConfig) => {
if (!config) {
config = {};
}
if (!config.headers) {
config.headers = {};
}
const token = localStorage.getItem("token");
if (token) {
config.headers.authorization = `Bearer ${token}`;
}
config.baseURL = "http://localhost:3001/api/";
return config;
});
const methods = {
get: axios.get,
post: axios.post,
put: axios.put,
delete: axios.delete,
patch: axios.patch,
};
export default methods;

View File

@@ -2829,6 +2829,14 @@ axe-core@^4.4.3:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f"
integrity sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==
axios@^0.27.2:
version "0.27.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972"
integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==
dependencies:
follow-redirects "^1.14.9"
form-data "^4.0.0"
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -4792,7 +4800,7 @@ flowbite@^1.5.3:
"@popperjs/core" "^2.9.3"
mini-svg-data-uri "^1.4.3"
follow-redirects@^1.0.0:
follow-redirects@^1.0.0, follow-redirects@^1.14.9:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
@@ -4825,6 +4833,15 @@ form-data@^3.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"