From 61c664e805023fd9c60a56a52c82e12030f5d2af Mon Sep 17 00:00:00 2001 From: Leander Tolksdorf <leander.tolksdorf@fu-berlin.de> Date: Mon, 31 Jan 2022 17:57:57 +0100 Subject: [PATCH] add login and logout feature --- client/src/api/auth.ts | 16 ++ client/src/api/user.ts | 9 + client/src/components/StaffLayout.tsx | 111 ++++++++---- client/src/pages/staff/StaffLogin.tsx | 191 ++++++++++++--------- server/src/controllers/auth.controllers.ts | 19 +- server/src/routes/auth.routes.ts | 1 + 6 files changed, 229 insertions(+), 118 deletions(-) create mode 100644 client/src/api/auth.ts create mode 100644 client/src/api/user.ts diff --git a/client/src/api/auth.ts b/client/src/api/auth.ts new file mode 100644 index 0000000..f57da51 --- /dev/null +++ b/client/src/api/auth.ts @@ -0,0 +1,16 @@ +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(); +} diff --git a/client/src/api/user.ts b/client/src/api/user.ts new file mode 100644 index 0000000..aa33194 --- /dev/null +++ b/client/src/api/user.ts @@ -0,0 +1,9 @@ +export async function getCurrentUser() { + const response = await fetch("/api/user", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + return response; +} diff --git a/client/src/components/StaffLayout.tsx b/client/src/components/StaffLayout.tsx index f54b1d8..d943594 100644 --- a/client/src/components/StaffLayout.tsx +++ b/client/src/components/StaffLayout.tsx @@ -1,42 +1,87 @@ 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; diff --git a/client/src/pages/staff/StaffLogin.tsx b/client/src/pages/staff/StaffLogin.tsx index dae8a92..de86ed9 100644 --- a/client/src/pages/staff/StaffLogin.tsx +++ b/client/src/pages/staff/StaffLogin.tsx @@ -1,7 +1,11 @@ +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; diff --git a/server/src/controllers/auth.controllers.ts b/server/src/controllers/auth.controllers.ts index b17e199..baadebb 100644 --- a/server/src/controllers/auth.controllers.ts +++ b/server/src/controllers/auth.controllers.ts @@ -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; diff --git a/server/src/routes/auth.routes.ts b/server/src/routes/auth.routes.ts index f6601bc..c8c1ca5 100644 --- a/server/src/routes/auth.routes.ts +++ b/server/src/routes/auth.routes.ts @@ -13,5 +13,6 @@ authRouter.post( handleValidationResult, authControllers.authLoginController ); +authRouter.post("/api/logout/", authControllers.authLogOutController); export default authRouter; -- GitLab