Connected authentication to the backend and fixed CORS
This commit is contained in:
parent
72123aee2f
commit
4667029869
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@ -22,3 +22,5 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
src/generated
|
16
frontend/codegen.yml
Normal file
16
frontend/codegen.yml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
overwrite: true
|
||||||
|
schema: "http://127.0.0.1:8000/graphql"
|
||||||
|
documents:
|
||||||
|
- 'src/queries/*.graphql'
|
||||||
|
generates:
|
||||||
|
./src/generated/graphql.ts:
|
||||||
|
plugins:
|
||||||
|
- typescript
|
||||||
|
- typescript-operations
|
||||||
|
- typescript-react-query
|
||||||
|
config:
|
||||||
|
fetcher:
|
||||||
|
fetchParams:
|
||||||
|
headers:
|
||||||
|
Content-Type: application/json
|
||||||
|
endpoint: "http://localhost:8000/graphql"
|
@ -5,27 +5,37 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"codegen": "graphql-codegen --config codegen.yml"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": ">=7.0.0 <8.0.0",
|
"@babel/core": ">=7.0.0 <8.0.0",
|
||||||
|
"@graphql-codegen/typed-document-node": "^2.2.11",
|
||||||
|
"@graphql-codegen/typescript-operations": "^2.4.0",
|
||||||
|
"@graphql-codegen/typescript-react-query": "^3.5.12",
|
||||||
"@headlessui/react": "^1.6.3",
|
"@headlessui/react": "^1.6.3",
|
||||||
"@heroicons/react": "^1.0.6",
|
"@heroicons/react": "^1.0.6",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
|
"graphql": "^16.5.0",
|
||||||
|
"graphql-tag": "^2.12.6",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
|
"react-hook-form": "^7.31.3",
|
||||||
"react-query": "^3.39.0",
|
"react-query": "^3.39.0",
|
||||||
"react-router-dom": "6"
|
"react-router-dom": "6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^7.1.0",
|
"@faker-js/faker": "^7.1.0",
|
||||||
|
"@graphql-codegen/cli": "^2.6.2",
|
||||||
"@types/react": "^18.0.0",
|
"@types/react": "^18.0.0",
|
||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
"@vitejs/plugin-react": "^1.3.0",
|
"@vitejs/plugin-react": "^1.3.0",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
"tailwindcss": "^3.0.24",
|
"tailwindcss": "^3.0.24",
|
||||||
"typescript": "^4.6.3",
|
"typescript": "^4.6.3",
|
||||||
"vite": "^2.9.9"
|
"vite": "^2.9.9"
|
||||||
}
|
},
|
||||||
|
"proxy": "http://localhost:4000"
|
||||||
}
|
}
|
3020
frontend/pnpm-lock.yaml
generated
3020
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -3,18 +3,22 @@ import MiddlewarePage from "./components/pages/middleware";
|
|||||||
import RoutersPage from "./components/pages/routers";
|
import RoutersPage from "./components/pages/routers";
|
||||||
import SecretsPage from "./components/pages/secrets";
|
import SecretsPage from "./components/pages/secrets";
|
||||||
import ServicesPage from "./components/pages/services";
|
import ServicesPage from "./components/pages/services";
|
||||||
import SetupPage from "./components/pages/setup";
|
import LoginPage from "./components/pages/login";
|
||||||
|
import RequireAuth from "./components/layouts/RequireAuth";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/services" element={<ServicesPage />} />
|
<Route element={<RequireAuth />}>
|
||||||
<Route path="/routers" element={<RoutersPage />} />
|
<Route path="/services" element={<ServicesPage />} />
|
||||||
<Route path="/middleware" element={<MiddlewarePage />} />
|
<Route path="/routers" element={<RoutersPage />} />
|
||||||
<Route path="/setup" element={<SetupPage />} />
|
<Route path="/middleware" element={<MiddlewarePage />} />
|
||||||
<Route path="/secrets" element={<SecretsPage />} />
|
<Route path="/secrets" element={<SecretsPage />} />
|
||||||
<Route path="*" element={<Navigate to="/services" />} />
|
<Route path="/" element={<Navigate to="/services" />} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/*" element={<LoginPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 27 KiB |
@ -7,6 +7,7 @@ interface InputProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
size?: InputSize;
|
size?: InputSize;
|
||||||
|
type?: "button" | "submit" | "reset";
|
||||||
}
|
}
|
||||||
|
|
||||||
const getButtonSize = (size?: InputSize) => {
|
const getButtonSize = (size?: InputSize) => {
|
||||||
@ -28,6 +29,7 @@ const Button: FC<InputProps> = (props) => {
|
|||||||
btnClassName,
|
btnClassName,
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
|
type={props.type}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</button>
|
</button>
|
||||||
|
@ -11,7 +11,8 @@ interface InputProps {
|
|||||||
name?: string;
|
name?: string;
|
||||||
size?: InputSize;
|
size?: InputSize;
|
||||||
value?: string;
|
value?: string;
|
||||||
onInput?: React.FormEventHandler<HTMLInputElement>;
|
onChange?: React.FormEventHandler<HTMLInputElement>;
|
||||||
|
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInputSize = (size?: InputSize) => {
|
const getInputSize = (size?: InputSize) => {
|
||||||
@ -31,7 +32,8 @@ const Input: FC<InputProps> = (props) => {
|
|||||||
name={props.name}
|
name={props.name}
|
||||||
type={props.type}
|
type={props.type}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
onInput={props.onInput}
|
onChange={props.onChange}
|
||||||
|
onBlur={props.onBlur}
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
required={props.required}
|
required={props.required}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
16
frontend/src/components/layouts/RequireAuth.tsx
Normal file
16
frontend/src/components/layouts/RequireAuth.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Outlet, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
const RequireAuth = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!localStorage.getItem("token")) {
|
||||||
|
navigate("/login");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <Outlet />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RequireAuth;
|
@ -48,20 +48,6 @@ const Sidebar = () => {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="hidden rounded-lg focus:outline-none focus:shadow-outline">
|
|
||||||
<svg fill="currentColor" viewBox="0 0 20 20" className="w-6 h-6">
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col flex-grow px-4 mt-8 space-y-2">
|
<div className="flex flex-col flex-grow px-4 mt-8 space-y-2">
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
@ -118,7 +104,13 @@ const Sidebar = () => {
|
|||||||
<p>nikuu</p>
|
<p>nikuu</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-6 h-6">
|
<div className="w-6 h-6">
|
||||||
<LogoutIcon />
|
<LogoutIcon
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
navigate("/login");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,8 @@ interface TextFieldProps {
|
|||||||
size?: InputSize;
|
size?: InputSize;
|
||||||
label?: string;
|
label?: string;
|
||||||
inputClassName?: string;
|
inputClassName?: string;
|
||||||
|
onChange?: React.FormEventHandler<HTMLInputElement>;
|
||||||
|
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextField: FC<TextFieldProps> = (props) => {
|
const TextField: FC<TextFieldProps> = (props) => {
|
||||||
@ -29,6 +31,8 @@ const TextField: FC<TextFieldProps> = (props) => {
|
|||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
required={props.required}
|
required={props.required}
|
||||||
size={props.size}
|
size={props.size}
|
||||||
|
onChange={props.onChange}
|
||||||
|
onBlur={props.onBlur}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
90
frontend/src/components/pages/login/index.tsx
Normal file
90
frontend/src/components/pages/login/index.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useLoginMutation } from "../../../generated/graphql";
|
||||||
|
|
||||||
|
import Button from "../../atoms/Button";
|
||||||
|
import TextField from "../../molecules/TextField";
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm();
|
||||||
|
const navigator = useNavigate();
|
||||||
|
const loginMutation = useLoginMutation();
|
||||||
|
const [submitError, setSubmitError] = useState("");
|
||||||
|
|
||||||
|
const onSubmit = ({ name, password }: any) => {
|
||||||
|
loginMutation.mutate({ credentials: { name, password } });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loginMutation.data) return;
|
||||||
|
if (Object.hasOwn(loginMutation.data.login, "message")) {
|
||||||
|
setSubmitError(loginMutation.data.login.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Object.hasOwn(loginMutation.data.login, "token")) {
|
||||||
|
localStorage.setItem("token", loginMutation.data.login.token);
|
||||||
|
navigator("/services");
|
||||||
|
}
|
||||||
|
}, [loginMutation.data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<div className="flex flex-col justify-center min-h-screen py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
|
<div className="flex flex-row justify-start items-center w-full space-x-2">
|
||||||
|
<div className="w-16 h-16 bg-sky-300 rounded-md" />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl">Traefik</h1>
|
||||||
|
<h2 className="block text-2xl font-extrabold tracking-tighter text-gray-900 transition duration-500 ease-in-out transform hover:text-gray-900">
|
||||||
|
Confman
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="py-10">
|
||||||
|
<form
|
||||||
|
className="space-y-6"
|
||||||
|
action="#"
|
||||||
|
method="POST"
|
||||||
|
data-bitwarden-watching="1"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="name"
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField label="Username" placeholder="name" {...field} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="password"
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
type="password"
|
||||||
|
label="Password"
|
||||||
|
placeholder="*****"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button type="submit">Login</Button>
|
||||||
|
{submitError && <span>{submitError}</span>}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
@ -1,62 +0,0 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import Button from "../../atoms/Button";
|
|
||||||
import TextField from "../../molecules/TextField";
|
|
||||||
import TraefikImgUrl from "../../../assets/traefik.png";
|
|
||||||
|
|
||||||
const SetupPage = () => {
|
|
||||||
const navigator = useNavigate();
|
|
||||||
|
|
||||||
const handleContinue = () => navigator("/");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main>
|
|
||||||
<div className="flex flex-col justify-center min-h-screen py-12 sm:px-6 lg:px-8">
|
|
||||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
|
||||||
<img className="w-40 h-40 mx-auto" src={TraefikImgUrl} />
|
|
||||||
<h2 className="mt-6 text-3xl font-extrabold text-center text-neutral-600">
|
|
||||||
Setup Traefik Confman
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
|
||||||
<div className="px-4 py-8 sm:px-10">
|
|
||||||
<form
|
|
||||||
className="space-y-6"
|
|
||||||
action="#"
|
|
||||||
method="POST"
|
|
||||||
data-bitwarden-watching="1"
|
|
||||||
>
|
|
||||||
<TextField
|
|
||||||
id="username"
|
|
||||||
label="Username"
|
|
||||||
placeholder="name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
label="Password"
|
|
||||||
placeholder="*****"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
id="repeatPassword"
|
|
||||||
type="password"
|
|
||||||
label="Repeat Password"
|
|
||||||
placeholder="*****"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button onClick={handleContinue}>Continue</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SetupPage;
|
|
13
frontend/src/queries/services.graphql
Normal file
13
frontend/src/queries/services.graphql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
mutation Login($credentials: LoginInput!) {
|
||||||
|
login(body: $credentials) {
|
||||||
|
... on AuthSuccess {
|
||||||
|
user {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
token
|
||||||
|
}
|
||||||
|
... on CommonError {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
main.py
13
main.py
@ -1,4 +1,5 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from strawberry.fastapi import GraphQLRouter
|
from strawberry.fastapi import GraphQLRouter
|
||||||
|
|
||||||
from api.schema import schema
|
from api.schema import schema
|
||||||
@ -13,5 +14,17 @@ graphql_app = GraphQLRouter(
|
|||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
|
origins = [
|
||||||
|
"http://localhost:3000",
|
||||||
|
]
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
app.include_router(graphql_app, prefix="/graphql")
|
app.include_router(graphql_app, prefix="/graphql")
|
||||||
app.mount("/", SPAStaticFiles(directory="frontend/dist", html=True), name="frontend")
|
app.mount("/", SPAStaticFiles(directory="frontend/dist", html=True), name="frontend")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user