Skip to content
Snippets Groups Projects
Commit 61c664e8 authored by Leander Tolksdorf's avatar Leander Tolksdorf
Browse files

add login and logout feature

parent 50e4c015
No related branches found
No related tags found
No related merge requests found
type StaffLoginParameters = {
email: string;
password: string;
};
export async function staffLogin({ email, password }: StaffLoginParameters) {
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ email: email, password: password }),
headers: {
"Content-Type": "application/json",
},
});
return response.json();
}
export async function getCurrentUser() {
const response = await fetch("/api/user", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
return response;
}
import { Container, Nav, Navbar } from "react-bootstrap";
import { useTranslation } from "react-i18next";
import { Outlet, useLocation, NavLink } from "react-router-dom";
import { Outlet, useLocation, NavLink, useNavigate } from "react-router-dom";
import { Helmet } from "react-helmet-async";
import { useEffect } from "react";
import { getCurrentUser } from "../api/user";
function StaffLayout() {
const { t } = useTranslation();
const {pathname} = useLocation();
return (
<div className="h-100 w-100">
<Helmet>
<title>{t(`routes.${pathname}`)}</title>
</Helmet>
<Navbar sticky="top" className="bg-secondary" variant="dark" expand="lg">
<Container>
<Navbar.Brand className="fw-bold" style={{minWidth: "200px"}}>{t(`routes.${pathname}`)}</Navbar.Brand>
<Navbar.Toggle />
<Navbar.Collapse>
<Nav className="mr-auto d-flex justify-content-between w-100">
<div className="d-flex">
<Nav activeKey={pathname}>
<Nav.Link as={NavLink} to="/staff/overview">{t("staffNav.buttonBoatOverview")}</Nav.Link>
<Nav.Link as={NavLink} to="/staff/manage">{t("staffNav.buttonManageBoats")}</Nav.Link>
<Nav.Link as={NavLink} to="/staff/boattypes">{t("staffNav.buttonBoatTypes")}</Nav.Link>
<Nav.Link as={NavLink} to="/staff/sports">{t("staffNav.buttonSports")}</Nav.Link>
<Nav.Link as={NavLink} to="/staff/statistics">{t("staffNav.buttonStatistics")}</Nav.Link>
</Nav>
</div>
<div>
<Nav.Link>{t("staffNav.buttonLogout")}</Nav.Link>
const { t } = useTranslation();
const { pathname } = useLocation();
const navigate = useNavigate();
</div>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
<Outlet />
</div>
);
async function validateCurrentUser() {
try {
const response = await getCurrentUser();
if (response.status !== 200) {
navigate("/login");
}
} catch (e) {}
}
async function logout() {
try {
const response = await fetch("/api/logout", {
method: "POST",
});
navigate("/");
} catch (e) {
console.log(e);
}
}
useEffect(() => {
validateCurrentUser();
}, []);
return (
<div className="h-100 w-100">
<Helmet>
<title>{t(`routes.${pathname}`)}</title>
</Helmet>
<Navbar sticky="top" className="bg-secondary" variant="dark" expand="lg">
<Container>
<Navbar.Brand className="fw-bold" style={{ minWidth: "200px" }}>
{t(`routes.${pathname}`)}
</Navbar.Brand>
<Navbar.Toggle />
<Navbar.Collapse>
<Nav className="mr-auto d-flex justify-content-between w-100">
<div className="d-flex">
<Nav activeKey={pathname}>
<Nav.Link as={NavLink} to="/staff/overview">
{t("staffNav.buttonBoatOverview")}
</Nav.Link>
<Nav.Link as={NavLink} to="/staff/manage">
{t("staffNav.buttonManageBoats")}
</Nav.Link>
<Nav.Link as={NavLink} to="/staff/boattypes">
{t("staffNav.buttonBoatTypes")}
</Nav.Link>
<Nav.Link as={NavLink} to="/staff/sports">
{t("staffNav.buttonSports")}
</Nav.Link>
<Nav.Link as={NavLink} to="/staff/statistics">
{t("staffNav.buttonStatistics")}
</Nav.Link>
</Nav>
</div>
<div>
<Nav.Link
onClick={() => {
logout();
}}
>
{t("staffNav.buttonLogout")}
</Nav.Link>
</div>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
<Outlet />
</div>
);
}
export default StaffLayout;
import { useEffect } from "react";
import { Button, Col, Container, Form, Row } from "react-bootstrap";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import validator from "validator";
import { staffLogin } from "../../api/auth";
import { getCurrentUser } from "../../api/user";
import Divider from "../../components/layout/Divider";
import Modal from "../../components/Modal";
......@@ -11,94 +15,113 @@ type FormData = {
};
function Login() {
const {
control,
handleSubmit,
formState: { errors },
}= useForm<FormData>({
mode: "onBlur"});
const { t } = useTranslation();
const {
control,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
mode: "onBlur",
});
const { t } = useTranslation();
const navigate = useNavigate();
const onSubmit = (data: FormData) => {
try {
/**
* Logic to handle authentication. Should be separated into a service in another file.
* await login(data);
*
*
* navigate("/boatManager");
*/
} catch (e) {
alert("error");
const onSubmit = async (data: FormData) => {
try {
const response = await staffLogin(data);
if (response.success) {
navigate("/staff", { replace: true });
}
};
} catch (e) {
// TODO: Error handling
}
};
return (
<Container className="position-absolute top-50 start-50 translate-middle">
<Row>
<Col xs={{ span: 10, offset: 1 }}>
<Modal>
<h1 className="text-center p-1">{t("staffLogin.title")}</h1>
<p className="text-center">{t("staffLogin.subtitle")}</p>
<Divider />
<Form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="email"
control={control}
defaultValue=""
render={({ field }) => (
<div className="mb-2">
<Form.Label>{t("staffLogin.labelEmail")}</Form.Label>
<Form.Control type="email" {...field} isInvalid={!!errors.email}/>
<Form.Control.Feedback type="invalid">
{errors.email?.message}
</Form.Control.Feedback>
</div>
)}
rules={{
required: {
value: true,
message: t("staffLogin.messages.required", {
val: t("staffLogin.labelEmail"),
}),
},
validate: (value: string) =>
validator.isEmail(value) ||
t("staffLogin.messages.invalidEmail").toString(),
}}
/>
<Controller
name="password"
control={control}
defaultValue=""
render={({ field }) => (
<div className="mb-2">
<Form.Label>{t("staffLogin.labelPassword")}</Form.Label>
<Form.Control type="password" {...field} isInvalid={!!errors.password}/>
<Form.Control.Feedback type="invalid">
{errors.password?.message}
</Form.Control.Feedback>
</div>
)}
rules={{
required: {
value: true,
message: t("staffLogin.messages.required", {
val: t("staffLogin.labelPassword"),
}),
}
}}
/>
<Button type="submit" variant="secondary" className="mt-2 w-100">
{t("staffLogin.buttonSignIn")}
</Button>
</Form>
</Modal>
</Col>
</Row>
</Container>
);
async function validateCurrentUser() {
try {
const response = await getCurrentUser();
if (response.status == 200) {
navigate("/staff");
}
} catch (e) {}
}
useEffect(() => {
validateCurrentUser();
}, []);
return (
<Container className="position-absolute top-50 start-50 translate-middle">
<Row>
<Col xs={{ span: 10, offset: 1 }}>
<Modal>
<h1 className="text-center p-1">{t("staffLogin.title")}</h1>
<p className="text-center">{t("staffLogin.subtitle")}</p>
<Divider />
<Form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="email"
control={control}
defaultValue=""
render={({ field }) => (
<div className="mb-2">
<Form.Label>{t("staffLogin.labelEmail")}</Form.Label>
<Form.Control
type="email"
{...field}
isInvalid={!!errors.email}
/>
<Form.Control.Feedback type="invalid">
{errors.email?.message}
</Form.Control.Feedback>
</div>
)}
rules={{
required: {
value: true,
message: t("staffLogin.messages.required", {
val: t("staffLogin.labelEmail"),
}),
},
validate: (value: string) =>
validator.isEmail(value) ||
t("staffLogin.messages.invalidEmail").toString(),
}}
/>
<Controller
name="password"
control={control}
defaultValue=""
render={({ field }) => (
<div className="mb-2">
<Form.Label>{t("staffLogin.labelPassword")}</Form.Label>
<Form.Control
type="password"
{...field}
isInvalid={!!errors.password}
/>
<Form.Control.Feedback type="invalid">
{errors.password?.message}
</Form.Control.Feedback>
</div>
)}
rules={{
required: {
value: true,
message: t("staffLogin.messages.required", {
val: t("staffLogin.labelPassword"),
}),
},
}}
/>
<Button type="submit" variant="secondary" className="mt-2 w-100">
{t("staffLogin.buttonSignIn")}
</Button>
</Form>
</Modal>
</Col>
</Row>
</Container>
);
}
export default Login;
......@@ -30,7 +30,13 @@ const authLoginController = async (req: Request, res: Response) => {
const token = jwt.sign(payload, envVars.JWT_SECRET);
return res.cookie("token", token).json({ success: true });
return res
.cookie("token", token, {
httpOnly: true,
secure: process.env.NOVE_ENV === "production",
path: "/",
})
.json({ success: true });
} else {
return res.status(401).json({ success: false, error: "invalidPassword" });
}
......@@ -39,9 +45,20 @@ const authLoginController = async (req: Request, res: Response) => {
return res.status(500).json({ success: false, error: "serverError" });
}
};
const authLogOutController = async (req: Request, res: Response) => {
return res
.cookie("token", null, {
httpOnly: true,
secure: process.env.NOVE_ENV === "production",
path: "/",
expires: new Date(0),
})
.json({ success: true });
};
const authControllers = {
authLoginController,
authLogOutController,
};
export default authControllers;
......@@ -13,5 +13,6 @@ authRouter.post(
handleValidationResult,
authControllers.authLoginController
);
authRouter.post("/api/logout/", authControllers.authLogOutController);
export default authRouter;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment