mirror of
https://github.com/Pacerino/CaddyProxyManager.git
synced 2026-04-05 09:03:59 -04:00
[Frontend] Implement Login with JWT and axios
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
"@types/node": "^16.11.59",
|
"@types/node": "^16.11.59",
|
||||||
"@types/react": "^18.0.20",
|
"@types/react": "^18.0.20",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"axios": "^0.27.2",
|
||||||
"flowbite": "^1.5.3",
|
"flowbite": "^1.5.3",
|
||||||
"flowbite-react": "^0.1.11",
|
"flowbite-react": "^0.1.11",
|
||||||
"joi": "^17.6.0",
|
"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 React from "react";
|
||||||
import Layout from "./components/Layout"
|
import { localAuthProvider } from "./auth";
|
||||||
import { Routes, Route } from "react-router-dom";
|
import { TextInput, Button, /* Alert */ } from "flowbite-react";
|
||||||
|
import { useForm, SubmitHandler } from "react-hook-form";
|
||||||
|
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
import HostsPage from "./pages/Hosts"
|
import HostsPage from "./pages/Hosts";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<AuthProvider>
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Layout />}>
|
<Route element={<Layout />}>
|
||||||
<Route index element={<HostsPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<RequireAuth>
|
||||||
|
<HostsPage />
|
||||||
|
</RequireAuth>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</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 { Navbar, Button } from "flowbite-react";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet, useNavigate } from "react-router-dom";
|
||||||
|
import { useAuth } from "../App";
|
||||||
function Layout() {
|
function Layout() {
|
||||||
|
const auth = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Navbar fluid={true} rounded={false}>
|
<Navbar fluid={true} rounded={false}>
|
||||||
@@ -14,8 +18,18 @@ function Layout() {
|
|||||||
Caddy Proxy Manager
|
Caddy Proxy Manager
|
||||||
</span>
|
</span>
|
||||||
</Navbar.Brand>
|
</Navbar.Brand>
|
||||||
|
|
||||||
|
{auth.user && (
|
||||||
|
<div className="flex md:order-2">
|
||||||
|
<Button onClick={() => auth.signout(() => navigate("/"))}>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
<Navbar.Toggle />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Navbar.Collapse>
|
<Navbar.Collapse>
|
||||||
{/* <Navbar.Link href="/home">Home</Navbar.Link>
|
{/* <Navbar.Link href="/home">Home</Navbar.Link>
|
||||||
<Navbar.Link href="/hosts">Hosts</Navbar.Link> */}
|
<Navbar.Link href="/hosts">Hosts</Navbar.Link> */}
|
||||||
</Navbar.Collapse>
|
</Navbar.Collapse>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { joiResolver } from "@hookform/resolvers/joi";
|
|||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
import { HiAdjustments, HiTrash, HiDocumentAdd } from "react-icons/hi";
|
import { HiAdjustments, HiTrash, HiDocumentAdd } from "react-icons/hi";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import http, { RequestResponse } from "../utils/axios";
|
||||||
|
|
||||||
type Domain = {
|
type Domain = {
|
||||||
fqdn: string;
|
fqdn: string;
|
||||||
@@ -104,10 +105,9 @@ function HostsPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getHosts = async () => {
|
const getHosts = async () => {
|
||||||
return await fetch(`http://${window.location.hostname}:3001/api/hosts`)
|
return await http.get<RequestResponse>(`/hosts`)
|
||||||
.then((res) => res.json())
|
.then((res) => {
|
||||||
.then((json) => {
|
setHostData(res.data);
|
||||||
setHostData(json);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,22 +122,14 @@ function HostsPage() {
|
|||||||
upstreams: data.upstreams,
|
upstreams: data.upstreams,
|
||||||
};
|
};
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await fetch(`http://${window.location.hostname}:3001/api/hosts`, {
|
await http.post(`hosts`, jsonBody);
|
||||||
body: JSON.stringify(jsonBody),
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
if (res.status !== 200) {
|
|
||||||
// handle Error
|
|
||||||
}
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setModal(false);
|
setModal(false);
|
||||||
getHosts();
|
getHosts();
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteHost = async (hostID: number) => {
|
const deleteHost = async (hostID: number) => {
|
||||||
await fetch(`http://${window.location.hostname}:3001/api/hosts/${hostID}`, {
|
await http.delete(`/hosts/${hostID}`);
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
getHosts();
|
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"
|
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f"
|
||||||
integrity sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==
|
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:
|
axobject-query@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
|
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"
|
"@popperjs/core" "^2.9.3"
|
||||||
mini-svg-data-uri "^1.4.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"
|
version "1.15.2"
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
|
||||||
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
|
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
|
||||||
@@ -4825,6 +4833,15 @@ form-data@^3.0.0:
|
|||||||
combined-stream "^1.0.8"
|
combined-stream "^1.0.8"
|
||||||
mime-types "^2.1.12"
|
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:
|
forwarded@0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||||
|
|||||||
Reference in New Issue
Block a user