From c71bac5bd2c5009bb6b8aa1ac737d632ec4cf3d3 Mon Sep 17 00:00:00 2001 From: Pau Costa Date: Wed, 7 Feb 2024 17:39:32 +0100 Subject: [PATCH] :sparkles: Auth views Signed-off-by: Pau Costa --- client/src/App.test.tsx | 9 -- client/src/App.tsx | 26 ----- client/src/app/loginSlice.ts | 118 ++++++++++++++++++++ client/src/app/store.ts | 20 ++++ client/src/components/Copyright.tsx | 18 +++ client/src/components/Drawer.tsx | 45 ++++++++ client/src/components/StyledComponents.tsx | 5 + client/src/error-page.tsx | 29 +++++ client/src/index.tsx | 54 ++++++++- client/src/routes/Auth/authRoot.tsx | 54 +++++++++ client/src/routes/Auth/login.tsx | 117 ++++++++++++++++++++ client/src/routes/Auth/register.tsx | 123 +++++++++++++++++++++ client/src/routes/root.tsx | 32 ++++++ client/src/util/types.ts | 6 + client/tsconfig.json | 10 +- docker-compose.yml | 1 + server/src/index.ts | 34 +++--- 17 files changed, 637 insertions(+), 64 deletions(-) delete mode 100644 client/src/App.test.tsx delete mode 100644 client/src/App.tsx create mode 100644 client/src/app/loginSlice.ts create mode 100644 client/src/app/store.ts create mode 100644 client/src/components/Copyright.tsx create mode 100644 client/src/components/Drawer.tsx create mode 100644 client/src/components/StyledComponents.tsx create mode 100644 client/src/error-page.tsx create mode 100644 client/src/routes/Auth/authRoot.tsx create mode 100644 client/src/routes/Auth/login.tsx create mode 100644 client/src/routes/Auth/register.tsx create mode 100644 client/src/routes/root.tsx create mode 100644 client/src/util/types.ts diff --git a/client/src/App.test.tsx b/client/src/App.test.tsx deleted file mode 100644 index 2a68616..0000000 --- a/client/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/client/src/App.tsx b/client/src/App.tsx deleted file mode 100644 index a53698a..0000000 --- a/client/src/App.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import logo from './logo.svg'; -import './App.css'; - -function App() { - return ( -
-
- logo -

- Edit src/App.tsx and save to reload. -

- - Learn React - -
-
- ); -} - -export default App; diff --git a/client/src/app/loginSlice.ts b/client/src/app/loginSlice.ts new file mode 100644 index 0000000..35b5d6b --- /dev/null +++ b/client/src/app/loginSlice.ts @@ -0,0 +1,118 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { Status } from "../util/types"; +import { + AuthLoginPostRequest, + AuthSignupPostRequest, + AuthenticationApi, + Configuration, +} from "../api"; +import { AppThunk } from "./store"; +import { Axios, AxiosError } from "axios"; + +interface loginState { + loggedIn: boolean; + status: Status; + error: string | null; + userInfo: { + firstName: string; + jwt: string; + }; +} + +const initialState: loginState = { + loggedIn: false, + status: Status.idle, + error: null, + userInfo: { + firstName: "", + jwt: "", + }, +}; + +export const loginSlice = createSlice({ + name: "login", + initialState, + reducers: { + login: (state, action) => { + state.loggedIn = true; + state.userInfo.jwt = action.payload; + }, + logoff: (state) => { + state.loggedIn = false; + state.userInfo = initialState.userInfo; + }, + setStatus: (state, action) => { + state.status = action.payload; + }, + setError: (state, action) => { + state.error = action.payload; + }, + }, +}); + +const api = new AuthenticationApi( + new Configuration({ + basePath: process.env.REACT_APP_BACKEND_URL, + }) +); + +export const postLogin = + (params: AuthLoginPostRequest): AppThunk => + async (dispatch) => { + let response; + try { + dispatch(setStatus(Status.loading)); + response = await api.authLoginPost(params); + + dispatch(login(response.data.token)); + await addJWT(response.data.token || ""); + + dispatch(setError("")); + dispatch(setStatus(Status.succeeded)); + } catch (error) { + dispatch(setStatus(Status.failed)); + const errorMessage = "Invalid email or password"; + dispatch(setError(errorMessage)); + } + }; + +export const postSignup = + (params: AuthSignupPostRequest): AppThunk => + async (dispatch) => { + let response; + console.log(params); + try { + dispatch(setStatus(Status.loading)); + response = await api.authSignupPost(params); + + dispatch(postLogin({ email: params.email, password: params.password })); + } catch (error) { + dispatch(setStatus(Status.failed)); + const errorMessage = "Change this pls"; + dispatch(setError(errorMessage)); + } + }; + +export const postLogout = (): AppThunk => async (dispatch) => { + localStorage.removeItem("jwt"); + sessionStorage.removeItem("jwt"); + dispatch(logoff()); + await api.authLogoutGet(); +}; + +const addJWT = async (token: string) => { + localStorage.setItem("jwt", token); +}; + +export const { login, logoff, setStatus, setError } = loginSlice.actions; + +export default loginSlice.reducer; + +export const selectLoggedIn = (state: { login: loginState }) => + state.login.loggedIn; + +export const selectUserInfo = (state: { login: loginState }) => + state.login.userInfo; + +export const selectErrorMessage = (state: { login: loginState }) => + state.login.error; diff --git a/client/src/app/store.ts b/client/src/app/store.ts new file mode 100644 index 0000000..68ccd64 --- /dev/null +++ b/client/src/app/store.ts @@ -0,0 +1,20 @@ +import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit"; +import { useDispatch } from "react-redux"; +import loginReducer from "./loginSlice"; + +export const store = configureStore({ + reducer: { + login: loginReducer, + }, +}); + +export type AppDispatch = typeof store.dispatch; +export type RootState = ReturnType; +export type AppThunk = ThunkAction< + ReturnType, + RootState, + unknown, + Action +>; + +export const useAppDispatch: () => AppDispatch = useDispatch; diff --git a/client/src/components/Copyright.tsx b/client/src/components/Copyright.tsx new file mode 100644 index 0000000..825d9f5 --- /dev/null +++ b/client/src/components/Copyright.tsx @@ -0,0 +1,18 @@ +import { Link, Typography } from "@mui/material"; + +export function Copyright(props: any) { + return ( + + {"Copyright © "} + + Tu Trastero Tu Otro Espacio S.L + {" "} + {new Date().getFullYear()}. + + ); +} diff --git a/client/src/components/Drawer.tsx b/client/src/components/Drawer.tsx new file mode 100644 index 0000000..2ce0708 --- /dev/null +++ b/client/src/components/Drawer.tsx @@ -0,0 +1,45 @@ +import {Grid, Hidden, Typography, Drawer, Box} from "@mui/material"; + +export default function ResponsiveDrawer(){ + const drawer = ( +
+ + + Home + + + Search + + + Notifications + + +
+ ); + + return ( + + {/* The implementation can be swapped with js to avoid SEO duplication of links. */} + + + {drawer} + + + + + {drawer} + + + + ); +} \ No newline at end of file diff --git a/client/src/components/StyledComponents.tsx b/client/src/components/StyledComponents.tsx new file mode 100644 index 0000000..74a67c2 --- /dev/null +++ b/client/src/components/StyledComponents.tsx @@ -0,0 +1,5 @@ +import { Divider, styled } from "@mui/material"; + +export const StyledDivider = styled(Divider)({ + marginBottom: "2%", +}); diff --git a/client/src/error-page.tsx b/client/src/error-page.tsx new file mode 100644 index 0000000..cfa7103 --- /dev/null +++ b/client/src/error-page.tsx @@ -0,0 +1,29 @@ +import {useRouteError} from "react-router-dom"; +import {Box, Typography} from "@mui/material"; + +export default function ErrorPage() { + const error = useRouteError() as any; + console.error(error); + + return ( +
+ + + Whoops! + + + Something went wrong :( + + + + {error.message || error.statusText} + + +
+ ) +} diff --git a/client/src/index.tsx b/client/src/index.tsx index 032464f..13737ff 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -1,15 +1,59 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +import "./index.css"; +import reportWebVitals from "./reportWebVitals"; +import "@fontsource/roboto"; +import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import Root from "./routes/root"; +import ErrorPage from "./error-page"; +import { createTheme, CssBaseline, ThemeProvider } from "@mui/material"; +import Login from "./routes/Auth/login"; +import Register from "./routes/Auth/register"; +import AuthRoot from "./routes/Auth/authRoot"; +import { Provider } from "react-redux"; +import { store } from "./app/store"; const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement + document.getElementById("root") as HTMLElement ); + +const defaultTheme = createTheme({ + palette: { + mode: "dark", + }, +}); + +const router = createBrowserRouter([ + { + path: "/", + element: , + errorElement: , + }, + { + path: "/auth", + element: , + errorElement: , + children: [ + { + path: "login", + element: , + }, + { + path: "register", + element: , + }, + ], + }, +]); + root.render( - + + + + + + ); diff --git a/client/src/routes/Auth/authRoot.tsx b/client/src/routes/Auth/authRoot.tsx new file mode 100644 index 0000000..641cd64 --- /dev/null +++ b/client/src/routes/Auth/authRoot.tsx @@ -0,0 +1,54 @@ +import { Copyright } from "@mui/icons-material"; +import { Grid, Paper, Typography } from "@mui/material"; +import { Outlet, useNavigate } from "react-router-dom"; +import { StyledDivider } from "../../components/StyledComponents"; +import { useSelector } from "react-redux"; +import { selectLoggedIn } from "../../app/loginSlice"; +import { useEffect } from "react"; + +export default function AuthRoot() { + const loggedIn = useSelector(selectLoggedIn); + const navigate = useNavigate(); + + useEffect(() => { + if (loggedIn) { + return navigate("/"); + } + }); + + return ( + + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900], + }} + direction="column" + spacing={5} + > + + + DevSpace + + + + + + + + + + + ); +} diff --git a/client/src/routes/Auth/login.tsx b/client/src/routes/Auth/login.tsx new file mode 100644 index 0000000..a7ef01a --- /dev/null +++ b/client/src/routes/Auth/login.tsx @@ -0,0 +1,117 @@ +import { + Box, + Button, + Checkbox, + Container, + FormControlLabel, + Grid, + Paper, + TextField, + Typography, + styled, +} from "@mui/material"; +import { StyledDivider } from "../../components/StyledComponents"; +import { Link } from "@mui/material"; +import { useAppDispatch } from "../../app/store"; +import { postLogin, selectErrorMessage } from "../../app/loginSlice"; +import { AuthLoginPostRequest } from "../../api"; +import { useSelector } from "react-redux"; + +export default function Login() { + const dispatch = useAppDispatch(); + const errorMessage = useSelector(selectErrorMessage); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const data = new FormData(event.currentTarget); + const paramsObject = { + email: data.get("email"), + password: data.get("password"), + longExpiration: data.get("longExpiration"), + }; + + dispatch(postLogin(paramsObject as unknown as AuthLoginPostRequest)); + }; + return ( + <> + + + + + + Email: + + + + Password: + + + + } + label="Remember me" + /> + + + + + + + + + + Don't have an account yet? + + + + + Register now! + + + + + + ); +} + +const StyledGrid = styled(Grid)({ + width: "50%", +}); + +const StyledTypography = styled(Typography)({ + marginBottom: "5%", +}); diff --git a/client/src/routes/Auth/register.tsx b/client/src/routes/Auth/register.tsx new file mode 100644 index 0000000..fb52abd --- /dev/null +++ b/client/src/routes/Auth/register.tsx @@ -0,0 +1,123 @@ +import { + Button, + Container, + Grid, + Link, + Paper, + TextField, + Typography, + styled, +} from "@mui/material"; +import { useAppDispatch } from "../../app/store"; +import { postSignup } from "../../app/loginSlice"; +import { AuthSignupPostRequest } from "../../api"; + +export default function Register() { + const dispatch = useAppDispatch(); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const data = new FormData(event.currentTarget); + const paramsObject = { + email: data.get("email"), + password: data.get("password"), + passwordValidation: data.get("passwordConfirmation"), + firstName: data.get("firstName"), + lastName: data.get("lastName"), + longExpiration: true, + }; + dispatch(postSignup(paramsObject as unknown as AuthSignupPostRequest)); + }; + + return ( + <> + + + + + First Name: + + + + Last Name: + + + + Email: + + + + Password: + + + + Password Confirmation: + + + + + + + + + Already have an account? + + + + + Login + + + + + + ); +} + +const StyledGrid = styled(Grid)({ + width: "50%", +}); +const StyledTypography = styled(Typography)({ + marginBottom: "5%", +}); diff --git a/client/src/routes/root.tsx b/client/src/routes/root.tsx new file mode 100644 index 0000000..f9f2014 --- /dev/null +++ b/client/src/routes/root.tsx @@ -0,0 +1,32 @@ +import ResponsiveDrawer from "../components/Drawer"; +import { Outlet, useActionData, useNavigate } from "react-router-dom"; +import { Button, Grid } from "@mui/material"; +import { postLogout, selectLoggedIn, selectUserInfo } from "../app/loginSlice"; +import { useAppDispatch } from "../app/store"; +import { useSelector } from "react-redux"; +import { useEffect } from "react"; + +export default function Root() { + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const loggedIn = useSelector(selectLoggedIn); + const userInfo = useSelector(selectUserInfo); + + useEffect(() => { + if (!loggedIn) { + navigate("/auth/login"); + } + }, [loggedIn, navigate]); + + const handleClick = () => { + dispatch(postLogout()); + }; + + return ( + <> + + + ); +} \ No newline at end of file diff --git a/client/src/util/types.ts b/client/src/util/types.ts new file mode 100644 index 0000000..571409c --- /dev/null +++ b/client/src/util/types.ts @@ -0,0 +1,6 @@ +export enum Status { + idle = "idle", + loading = "loading", + succeeded = "succeeded", + failed = "failed", +} diff --git a/client/tsconfig.json b/client/tsconfig.json index a273b0c..9d379a3 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -20,7 +16,5 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": [ - "src" - ] + "include": ["src"] } diff --git a/docker-compose.yml b/docker-compose.yml index b4445b6..31794b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: build: ./client environment: NODE_ENV: development + REACT_APP_BACKEND_URL: http://localhost:3000 ports: - "8080:3000" server: diff --git a/server/src/index.ts b/server/src/index.ts index b059b60..d7d355f 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -7,34 +7,36 @@ import {UserRoutes} from "./routes/userRoutes"; import {AuthController} from "./controller/authController"; import {PostRoutes} from "./routes/postRoutes"; import {swaggerRouter} from "./routes/swaggerRoutes"; +import * as cors from "cors"; -AppDataSource.initialize().then(async () => { - +AppDataSource.initialize() + .then(async () => { // create express app - const app = express() - app.use(bodyParser.json()) + const app = express(); + app.use(bodyParser.json()); + app.use(cors()); // register express routes from defined application routes // Auth Routes - app.use('/auth', AuthRoutes) + app.use("/auth", AuthRoutes); // Swagger Routes - app.use('/docs', swaggerRouter); + app.use("/docs", swaggerRouter); // All routes after this one require authentication const authController = new AuthController(); - app.use(authController.protect) + app.use(authController.protect); - app.use('/users', UserRoutes) - app.use('/posts', PostRoutes) + app.use("/users", UserRoutes); + app.use("/posts", PostRoutes); // setup express app here - app.use(errorHandler) + app.use(errorHandler); // start express server - app.listen(3000) + app.listen(3000); - - - console.log("Express server has started on port 3000. Open http://localhost:3000/users to see results") - -}).catch(error => console.log(error)) + console.log( + "Express server has started on port 3000. Open http://localhost:3000/users to see results" + ); + }) + .catch((error) => console.log(error));