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 (
-
- );
-}
-
-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));