mirror of
https://github.com/Pacerino/CaddyProxyManager.git
synced 2026-03-31 06:34:15 -04:00
[Frontend] Implement Login with JWT and axios
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
23
frontend/src/auth.ts
Normal 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 };
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
37
frontend/src/utils/axios.ts
Normal file
37
frontend/src/utils/axios.ts
Normal 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;
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user