diff --git a/frontend/package.json b/frontend/package.json index dfc5fd3..e7eeed2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 33eb29f..15ab2ed 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 ( - <> - + - }> - } /> + }> + } /> + + + + } + /> - + + ); +} + +interface AuthContextType { + user: any; + signin: (mail: string, password: string, callback: VoidFunction) => void; + signout: (callback: VoidFunction) => void; +} + +let AuthContext = React.createContext(null!); + +function AuthProvider({ children }: { children: React.ReactNode }) { + let [user, setUser] = React.useState(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 {children}; +} + +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 ; + } + + 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(); + const performLogin: SubmitHandler = async (data) => { + auth.signin(data.email, data.password, () => { + navigate(from, { replace: true }); + }) + }; + + return ( +
+
+
+

Please login

+ {/* {error && ( +
+ {error} +
+ )} */} +
+ +
+
+ +
+
+ +
+
+
+
); } diff --git a/frontend/src/auth.ts b/frontend/src/auth.ts new file mode 100644 index 0000000..901c3e9 --- /dev/null +++ b/frontend/src/auth.ts @@ -0,0 +1,23 @@ +import http, { RequestResponse} from "./utils/axios" + +const localAuthProvider = { + isAuthenticated: false, + signin(email: string, password: string, callback: VoidFunction) { + http + .post("/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 }; diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 71b5677..fcd5830 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -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 (
@@ -14,8 +18,18 @@ function Layout() { Caddy Proxy Manager + + {auth.user && ( +
+ + +
+ )} + -{/* Home + {/* Home Hosts */}
diff --git a/frontend/src/pages/Hosts.tsx b/frontend/src/pages/Hosts.tsx index 7e8c6b6..fdbb432 100644 --- a/frontend/src/pages/Hosts.tsx +++ b/frontend/src/pages/Hosts.tsx @@ -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(`/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(); }; diff --git a/frontend/src/utils/axios.ts b/frontend/src/utils/axios.ts new file mode 100644 index 0000000..43a5c2e --- /dev/null +++ b/frontend/src/utils/axios.ts @@ -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; \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 953a97e..8eb01a8 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -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"