diff --git a/sports-matcher/client/.eslintrc.json b/sports-matcher/client/.eslintrc.json index 3cdde28..3b3bf9a 100644 --- a/sports-matcher/client/.eslintrc.json +++ b/sports-matcher/client/.eslintrc.json @@ -25,7 +25,7 @@ 4 ], "linebreak-style": [ - "error", + "warn", "unix" ], "quotes": [ @@ -35,6 +35,7 @@ "semi": [ "error", "always" - ] + ], + "no-unused-vars": "warn" } } \ No newline at end of file diff --git a/sports-matcher/client/package-lock.json b/sports-matcher/client/package-lock.json index 3104365..65b29a5 100644 --- a/sports-matcher/client/package-lock.json +++ b/sports-matcher/client/package-lock.json @@ -18,6 +18,7 @@ "react-dom": "^17.0.2", "react-router-dom": "^6.2.2", "react-scripts": "5.0.0", + "validator": "^13.7.0", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -15382,6 +15383,14 @@ "node": ">= 8" } }, + "node_modules/validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -27295,6 +27304,11 @@ } } }, + "validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/sports-matcher/client/package.json b/sports-matcher/client/package.json index 1e67687..290931b 100644 --- a/sports-matcher/client/package.json +++ b/sports-matcher/client/package.json @@ -13,6 +13,7 @@ "react-dom": "^17.0.2", "react-router-dom": "^6.2.2", "react-scripts": "5.0.0", + "validator": "^13.7.0", "web-vitals": "^2.1.4" }, "scripts": { @@ -43,4 +44,4 @@ "eslint": "^8.12.0", "eslint-plugin-react": "^7.29.4" } -} \ No newline at end of file +} diff --git a/sports-matcher/client/src/Layout.js b/sports-matcher/client/src/Layout.js index 9864c39..3b83f9b 100644 --- a/sports-matcher/client/src/Layout.js +++ b/sports-matcher/client/src/Layout.js @@ -1,6 +1,6 @@ import "./styles/Layout.css"; import "./styles/extra.css"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { NavLink, Route, Routes, useNavigate } from "react-router-dom"; import Welcome from "./pages/Welcome"; import Navbar from "react-bootstrap/Navbar"; @@ -8,31 +8,22 @@ import { Container, Nav, NavbarBrand } from "react-bootstrap"; import NavbarToggle from "react-bootstrap/esm/NavbarToggle"; import NavbarCollapse from "react-bootstrap/esm/NavbarCollapse"; import Dashboard from "./pages/Dashboard"; +import Logout from "./pages/Logout"; +import Rentals from "./pages/Rentals"; +import Admin from "./pages/Administration"; import Login from "./pages/Login"; -import { apiClient } from "./utils/httpClients"; -import { globalContext } from "./context.js"; +import Context from "./globals.js"; +import Signup from "./pages/Signup"; export default function layout() { - const navigate = useNavigate(); - const [state, setState] = useState({ + const [globals, setGlobals] = useState({ user: null, + update: (updates, onUpdate) => setGlobals((state) => { return { ...state, ...updates }; }, onUpdate), + navigate: useNavigate() }); - useEffect(async () => { - await updateAuthStatus(); - }); - - async function updateAuthStatus() { - const getUserResponse = await apiClient.get("/user"); - if (getUserResponse !== 200) { - setState({ user: null }); - } else { - setState({ user: getUserResponse.data }); - } - } - - let indentityDisplay = ( + let identityDisplay = ( ); - if (state.user) { - indentityDisplay = ( + if (globals.user) { + identityDisplay = ( - {indentityDisplay} + {identityDisplay} @@ -80,12 +71,16 @@ export default function layout() { } /> } /> } /> + } /> + } /> + } /> + } />
- + ); } \ No newline at end of file diff --git a/sports-matcher/client/src/components/AuthenticationGuard.js b/sports-matcher/client/src/components/AuthenticationGuard.js new file mode 100644 index 0000000..130c473 --- /dev/null +++ b/sports-matcher/client/src/components/AuthenticationGuard.js @@ -0,0 +1,44 @@ +import React from "react"; +import globals from "../globals"; +import { apiClient } from "../utils/httpClients"; +import propTypes from "prop-types"; + +export default class AuthenticationGuard extends React.Component { + constructor(props) { + super(props); + } + static contextType = globals; + + async componentDidMount() { + let userDataResponse = await apiClient.get("/user"); + if (userDataResponse.status === 200) { + this.context.update({ user: userDataResponse.data }); + if (this.context.user && this.context.user.accessLevel < this.props.accessLevel) { + this.context.navigate("/", { replace: true }); + } + } else if (userDataResponse.status == 401) { + this.context.navigate("/signup", { replace: true }); + this.context.update({ user: null }); + } + } + + componentDidUpdate() { + } + + render() { + if (this.context.user) { + return this.props.children; + } else { + return null; + } + } +} + +AuthenticationGuard.defaultProps = { + accessLevel: 0 +}; + +AuthenticationGuard.propTypes = { + accessLevel: propTypes.number, + children: propTypes.any +}; \ No newline at end of file diff --git a/sports-matcher/client/src/components/RentalInfoCard.js b/sports-matcher/client/src/components/RentalInfoCard.js new file mode 100644 index 0000000..f31eff6 --- /dev/null +++ b/sports-matcher/client/src/components/RentalInfoCard.js @@ -0,0 +1,31 @@ +import React from "react"; +import { Card } from "react-bootstrap"; +//import { Button, Card } from "react-bootstrap"; +import propTypes from "prop-types"; +//import { grammaticalListString } from "../utils/strings"; +export default class MatchInfoCard extends React.Component { + constructor(props) { + super(props); + } + + + render() { + return ( + // + + + {this.props.rental.title} + Rate: {this.props.rental.rate} + Date Created: {this.props.rental.createDate} + Owner: {this.props.rental.creator} + Contact: {this.props.rental.contact} + Description: {this.props.rental.description} + + + ); + } +} + +MatchInfoCard.propTypes = { + rental: propTypes.object, +}; \ No newline at end of file diff --git a/sports-matcher/client/src/components/SportInfoCard.js b/sports-matcher/client/src/components/SportInfoCard.js index 14477e9..c1c909f 100644 --- a/sports-matcher/client/src/components/SportInfoCard.js +++ b/sports-matcher/client/src/components/SportInfoCard.js @@ -11,7 +11,7 @@ export default class SportInfoCard extends React.Component { {this.props.sport.name} - {this.props.sport.minPlayers.toString()} + Requires a minimum of {this.props.sport.minPlayers.toString()} players. {this.props.sport.description} diff --git a/sports-matcher/client/src/context.js b/sports-matcher/client/src/context.js deleted file mode 100644 index 8f6e61c..0000000 --- a/sports-matcher/client/src/context.js +++ /dev/null @@ -1,3 +0,0 @@ -import React from "react"; - -export const globalContext = React.createContext({}); diff --git a/sports-matcher/client/src/globals.js b/sports-matcher/client/src/globals.js new file mode 100644 index 0000000..ddf5ff9 --- /dev/null +++ b/sports-matcher/client/src/globals.js @@ -0,0 +1,7 @@ +import React from "react"; + +export default React.createContext({ + user: null, + update: () => { }, + navigate: () => { } +}); \ No newline at end of file diff --git a/sports-matcher/client/src/pages/Administration.js b/sports-matcher/client/src/pages/Administration.js new file mode 100644 index 0000000..e87576d --- /dev/null +++ b/sports-matcher/client/src/pages/Administration.js @@ -0,0 +1,251 @@ +import React from "react"; +import { Button, ButtonGroup, Spinner, Table } from "react-bootstrap"; +import "../styles/Admin.css"; +import globals from "../globals"; +import AuthenticationGuard from "../components/AuthenticationGuard"; +import { apiClient } from "../utils/httpClients"; + +export default class Admin extends React.Component { + constructor(props) { + super(props); + // Use null to indicate not loaded + // Use empty array to indicate no items for that state. + this.state = { + users: null, + suspendedUsers: null, + matches: null, + user: null, + currentTab: "matches", + }; + } + + static contextType = globals; + + async componentDidMount() { + await this.loadActiveUsers(); + await this.loadSuspendedUsers(); + await this.loadMatches(); + } + + async loadActiveUsers() { + let response = await apiClient.get("/user/all/active"); + if (response.status === 200) { + this.setState({ users: response.data.active }); + } + } + + async loadSuspendedUsers() { + let response = await apiClient.get("/user/all/suspended"); + if (response.status === 200) { + this.setState({ suspendedUsers: response.data.suspended }); + } else { + console.error(response.status); + } + } + + async loadMatches() { + let response = await apiClient.get("/match/all"); + if (response.status === 200) { + this.setState({ matches: response.data.all }); + } + } + + DeleteButton() { + return ; + + } + + PardonButton() { + return ; + + } + + EditButton() { + return ; + + } + + userTableHead() { + return ( + + + ID + Username + Name + Email + Phone + + + + + ); + } + + matchTableHead() { + return ( + + + ID + Sport + Date + Location + + + + + ); + } + + + userTableData() { + if (!this.state.users) { + return ( + + + + + + + + + + ); + } + return this.state.users.map((user) => { + const { _id, firstName, lastName, email, phone } = user; + return ( + + {_id} + {firstName} + {lastName} + {email} + {phone} + {this.DeleteButton()} + {this.EditButton()} + + ); + }); + } + + susUserTableData() { + if (!this.state.suspendedUsers) { + return ( + + + + + + + + + + + ); + } + + return this.state.suspendedUsers.map((user) => { + const { _id, firstName, lastName, email, phone } = user; + return ( + + {_id} + {firstName} + {lastName} + {email} + {phone} + {this.DeleteButton()} + {this.EditButton()} + {this.PardonButton()} + + ); + }); + } + + + matchTableData() { + if (!this.state.matches) { + return ( + + + + + + + + + ); + } + + return this.state.matches.map((match) => { + const { _id, sport, when, location } = match; + const sportName = sport.name; + return ( + + {_id} + {sportName} + {when} + {location} + {this.DeleteButton()} + {this.EditButton()} + + ); + }); + } + + renderTableHead() { + if (this.state.currentTab === "matches") { + return this.matchTableHead(); + } else if (this.state.currentTab === "users") { + return this.userTableHead(); + } else { + return this.userTableHead(); + } + } + + renderTableData() { + if (this.state.currentTab === "matches") { + return this.matchTableData(); + } else if (this.state.currentTab === "users") { + return this.userTableData(); + } else { + return this.susUserTableData(); + } + } + + render() { + return ( +
+ + +
+

Administration

+ + + + + +
+ + {this.renderTableHead()} + + {this.renderTableData()} + {/* {this.matchUserTableData()} */} + +
+
+
+
+ ); + } +} \ No newline at end of file diff --git a/sports-matcher/client/src/pages/Dashboard.js b/sports-matcher/client/src/pages/Dashboard.js index a8370a0..b88f5ca 100644 --- a/sports-matcher/client/src/pages/Dashboard.js +++ b/sports-matcher/client/src/pages/Dashboard.js @@ -4,8 +4,8 @@ import "../styles/Dashboard.css"; import { apiClient } from "../utils/httpClients.js"; import MatchInfoCardDisplay from "../components/MatchInfoCardDisplay"; import SportInfoCardDisplay from "../components/SportInfoCardDisplay"; -import { globalContext } from "../context"; -import { needUser } from "../utils/routing"; +import AuthenticationGuard from "../components/AuthenticationGuard"; +import globals from "../globals"; export default class Dashboard extends React.Component { constructor(props) { @@ -18,15 +18,15 @@ export default class Dashboard extends React.Component { }; } - static contextType = globalContext; + static contextType = globals; async componentDidMount() { - await needUser(this.context.navigate); + this.setState({ user: this.context.user }); await this.latestMatches(); await this.availableSports(); } async latestMatches() { - let recentMatchesRes = await apiClient.get("/match/recent/15"); + let recentMatchesRes = await apiClient.get("/match/recent/user/15"); if (recentMatchesRes.status === 200) { this.setState({ displayedMatches: recentMatchesRes.data.recent }); } @@ -41,29 +41,31 @@ export default class Dashboard extends React.Component { render() { return ( - -

- - - - -
-

Available Matches

- + +
+ +

+ + + + +
+

Available Matches

+ +
+
+

Available Sports

+ +
+
-
-

Available Sports

- -
- - - +
); } } \ No newline at end of file diff --git a/sports-matcher/client/src/pages/Login.js b/sports-matcher/client/src/pages/Login.js index 526852e..3a8d265 100644 --- a/sports-matcher/client/src/pages/Login.js +++ b/sports-matcher/client/src/pages/Login.js @@ -1,8 +1,7 @@ import React from "react"; import { Alert, Button, Card, Container, Form } from "react-bootstrap"; -import { globalContext } from "../context"; +import globals from "../globals"; import { apiClient } from "../utils/httpClients"; -import { guard } from "../utils/routing"; export default class Login extends React.Component { constructor(props) { @@ -16,16 +15,14 @@ export default class Login extends React.Component { this.attemptLogin = this.attemptLogin.bind(this); } - static contextType = globalContext; + static contextType = globals; async componentDidMount() { - try { - const getUserResponse = await apiClient.get("/user"); - guard(this.context.navigate, () => getUserResponse.status === 401, "/dashboard"); // If it's not 401, then we redirect to dashboard. - } catch (error) { - if (error.message !== "Request failed with status code 401") { - throw error; - } + } + + componentDidUpdate() { + if (this.context.user) { + this.context.navigate("/dashboard"); } } @@ -34,13 +31,9 @@ export default class Login extends React.Component { const loginResponse = await apiClient.post("/user/login", { email: this.state.email, password: this.state.password, - }, { - validateStatus: function (status) { - return status === 200 || status === 401 || status === 400; - } }); if (loginResponse.status === 200) { - this.context.navigate("/dashboard", { replace: true }); + this.context.update({ user: loginResponse.data }); } else if (loginResponse.status === 401) { this.setState({ errorDisplayed: true }); } diff --git a/sports-matcher/client/src/pages/Logout.js b/sports-matcher/client/src/pages/Logout.js index 517d95c..b9e098c 100644 --- a/sports-matcher/client/src/pages/Logout.js +++ b/sports-matcher/client/src/pages/Logout.js @@ -1,5 +1,5 @@ import React from "react"; -import { useNavigate } from "react-router-dom"; +import globals from "../globals"; import { apiClient } from "../utils/httpClients"; export default class Logout extends React.Component { @@ -7,19 +7,22 @@ export default class Logout extends React.Component { super(props); } + static contextType = globals; + async componentDidMount() { const logoutResponse = await apiClient.get("/user/logout"); - let navigation = useNavigate(); - if (logoutResponse.status === 401) { - navigation("/dashboard", { replace: true }); - } else { + if (logoutResponse.status === 200) { this.redirectTimer = setTimeout(() => { - navigation("/", { replace: true }); + this.context.navigate("/", { replace: true }); }, 2000); + } else if (logoutResponse.status == 401) { + this.context.navigate("/", { replace: true }); } + + this.context.update({ user: null }); } - async componentWillUnmount() { + componentWillUnmount() { clearTimeout(this.redirectTimer); } diff --git a/sports-matcher/client/src/pages/Profile.js b/sports-matcher/client/src/pages/Profile.js new file mode 100644 index 0000000..e7c6433 --- /dev/null +++ b/sports-matcher/client/src/pages/Profile.js @@ -0,0 +1,14 @@ +import React from "react"; +import { Container } from "react-bootstrap"; + +export default class Profile extends React.Component { + render() { + return ( +
+ + + +
+ ); + } +} \ No newline at end of file diff --git a/sports-matcher/client/src/pages/Rentals.js b/sports-matcher/client/src/pages/Rentals.js new file mode 100644 index 0000000..abfe5ad --- /dev/null +++ b/sports-matcher/client/src/pages/Rentals.js @@ -0,0 +1,89 @@ +import React from "react"; +import { Button, InputGroup, FormControl } from "react-bootstrap"; +import "../styles/Dashboard.css"; +// import { apiClient } from "../utils/httpClients.js"; +// import MatchInfoCardDisplay from "../components/MatchInfoCardDisplay"; +// import SportInfoCardDisplay from "../components/SportInfoCardDisplay"; +import RentalInfoCard from "../components/RentalInfoCard"; +// import AuthenticationGuard from "../components/AuthenticationGuard"; +// import globals from "../globals"; + +export default class Rentals extends React.Component { + constructor(props) { + super(props); + this.state = { + rentals: [ + { id: 9, creator: "Person5", createDate: "05/21/2022", title: "Horse", rate: "$1000/day", description: "This is an amazing horse, has won many races", contact: "647 765 1234" }, + { id: 7, creator: "Person1", createDate: "05/05/2022", title: "Tennis Racquet", rate: "$300/day", description: "This is an amazing tennis racquet, used by Roger Federer to win Wimbledon in 2003", contact: "123 456 7890" }, + { id: 3, creator: "Person2", createDate: "05/11/2022", title: "Soccer Ball", rate: "$70/day", description: "This is an amazing soccer ball, signed by Messi", contact: "647 822 4321" }, + { id: 2, creator: "Person3", createDate: "05/13/2022", title: "Basket Ball", rate: "$7/day", description: "This is an amazing basketball, same model as the ones used in the NBA", contact: "467 279 4321" }, + { id: 1, creator: "Person4", createDate: "05/18/2022", title: "Table Tennis Racquet", rate: "$7/day", description: "This is an amazing table tennis racquet, it's very good", contact: "326 111 4321" }, + + ] + }; + } + + // static contextType = globals; + + // async componentDidMount() { + // this.setState({ user: this.context.user }); + // await this.latestMatches(); + // await this.availableSports(); + // } + // async latestMatches() { + // let recentMatchesRes = await apiClient.get("/match/recent/user/15"); + // if (recentMatchesRes.status === 200) { + // this.setState({ displayedMatches: recentMatchesRes.data.recent }); + // } + // } + + // async availableSports() { + // let availableSportsRes = await apiClient.get("/sport"); + // if (availableSportsRes.status === 200) { + // this.setState({ displayedSports: availableSportsRes.data }); + // } + // } + + // renderRentals() { + // let matches = null; + // if (this.props.recommendedmatches.length > 0) { + // matches = this.props.recommendedmatches.map((match) => ); + // } + // return ( + //
+ // {matches} + //
+ // ); + // } + + rentalsCards() { + return this.state.rentals.map((rental) => { + return (); + }); + } + + render() { + return ( + +
+ +

+ + + + +
+

Available Rentals

+ {this.rentalsCards()} +
+
+
+ ); + } +} \ No newline at end of file diff --git a/sports-matcher/client/src/pages/SIgnup.js b/sports-matcher/client/src/pages/SIgnup.js deleted file mode 100644 index c603579..0000000 --- a/sports-matcher/client/src/pages/SIgnup.js +++ /dev/null @@ -1,88 +0,0 @@ -import React from "react"; -import { Button, Card, Form } from "react-bootstrap"; -import { apiClient } from "../utils/httpClients"; -import { guard } from "../utils/routing"; - -export default class Signup extends React.Component { - constructor(props) { - super(props); - this.state = { - user: null, - alertShow: false, - alertKey: null, - alertMsg: null - } - this.state.user = { - email: null, - firstName: null, - lastName: null, - phone: null, - password: null - } - } - - async registerUser() { - const res = await apiClient.post("/user", this.state); - if (res.status === 200) { - this.warnUser("You are successfully signed up!", "success") - } else if (res === 409) { - this.warnUser("This user already exists. Try logging in instead.", "danger") - } else if (res === 400) { - this.warnUser("Missing required fields.", "danger") - } else { - this.warnUser("Internal server error. Please try again later.", "danger") - } - } - - setUserState(event) { - newUser = this.state.user; - newUser[event.target.controlId] = event.target.value - this.setState({user: newUser}) - } - - warnUser(msg, key) { - this.setState({alertMsg: msg}) - this.setState({show: true}) - } - - render() { - return ( -
- - {this.state.alertMsg} - - - - Login - Welcome to Sports Matcher! -
- - First name - - - - Last name - - - - E-mail - - - - Phone number - - - - Password - - - -
-
-
-
- ); - } -} diff --git a/sports-matcher/client/src/pages/Signup.js b/sports-matcher/client/src/pages/Signup.js new file mode 100644 index 0000000..b5fa2b0 --- /dev/null +++ b/sports-matcher/client/src/pages/Signup.js @@ -0,0 +1,147 @@ +import React from "react"; +import { Alert, Button, Card, Container, Form } from "react-bootstrap"; +import { Link } from "react-router-dom"; +import validator from "validator"; +import globals from "../globals"; +import { apiClient } from "../utils/httpClients"; + +export default class Signup extends React.Component { + constructor(props) { + super(props); + this.state = { + user: { + email: null, + firstName: null, + lastName: null, + phone: null, + password: null + }, + alert: { + show: false, + variant: null, + headerMsg: null, + content: null + } + }; + + this.registerUser = this.registerUser.bind(this); + this.setUserState = this.setUserState.bind(this); + } + + static contextType = globals; + + async registerUser(event) { + event.preventDefault(); + let formIssues = this.validateCurrentForm(); + if (formIssues.length > 0) { + this.notifyUser("Oops there were issue(s)", ( +
    + {formIssues.map((issue) => { + return ( +
  • {issue}
  • + ); + })} +
+ ), "danger"); + return; + } + + const res = await apiClient.post("/user", this.state.user); + if (res.status === 201) { + this.notifyUser("Success!",
You are successfully signed up! You wil be directed to login now.
, "success"); + this.redirectTimer = setTimeout(() => { + this.context.navigate("/signin", { replace: true }); + }, 1000); + } else if (res.status === 409) { + this.notifyUser("User exists!",
This user already exists. Try logging in instead.
, "danger"); + } else if (res.status === 400) { + this.notifyUser("There were errors in the submitted info.",
Double check to see if everything is inputted is valid.
, "danger"); + } else { + this.notifyUser("Error",
Internal server error. Please try again later.
, "danger"); + } + } + + componentWillUnmount() { + clearTimeout(this.redirectTimer); + } + + validateCurrentForm() { + let formIssues = []; + if (!validator.isEmail(this.state.user.email)) { + formIssues.push("The email submitted is invalid."); + } + + if (this.state.user.password.length < 8) { + formIssues.push("The password submitted must have a minimum length of 8 characters."); + } + + return formIssues; + } + + setUserState(event) { + this.setState((state) => { + state.user[event.target.id] = event.target.value; + return state; + }); + } + + notifyUser(headerMsg, content, key) { + this.setState((state) => { + state.alert.show = true; + state.alert.headerMsg = headerMsg; + state.alert.content = content; + state.alert.key = key; + return state; + }); + } + + componentDidMount() { + if (this.context.user) { + this.context.navigate("/dashboard"); + } + } + + render() { + return ( +
+ + this.setState((state) => { state.alert.show = false; return state; })} dismissible> + {this.state.alert.headerMsg} + {this.state.alert.content} + + + + Sign up! + Welcome to Sports Matcher! Already have an account? +
+ + First name + + + + Last name + + + + E-mail + + + + Phone number + + + + Password + + + +
+
+
+
+
+ ); + } +} diff --git a/sports-matcher/client/src/styles/Admin.css b/sports-matcher/client/src/styles/Admin.css new file mode 100644 index 0000000..abbcc9b --- /dev/null +++ b/sports-matcher/client/src/styles/Admin.css @@ -0,0 +1,15 @@ + +.MainTable { + padding : 20px; +} + +.center { + text-align: center; + padding: 21px; +} + + +.somespace { + padding: 17px; +} + diff --git a/sports-matcher/client/src/utils/httpClients.js b/sports-matcher/client/src/utils/httpClients.js index 6cff00c..4e2ddcc 100644 --- a/sports-matcher/client/src/utils/httpClients.js +++ b/sports-matcher/client/src/utils/httpClients.js @@ -1,10 +1,10 @@ import axios from "axios"; export const apiClient = axios.create({ - baseURL: process.env.REACT_APP_API_HOST || "" + "/api/", + baseURL: (process.env.REACT_APP_API_HOST || "") + "/api/", timeout: 5000, withCredentials: process.env.NODE_ENV === "development", validateStatus: function (status) { - return status === 401 || status == 200; + return status === 401 || status === 200 || status === 400 || status === 201; } }); \ No newline at end of file diff --git a/sports-matcher/client/src/utils/routing.js b/sports-matcher/client/src/utils/routing.js deleted file mode 100644 index 46caad9..0000000 --- a/sports-matcher/client/src/utils/routing.js +++ /dev/null @@ -1,22 +0,0 @@ -import { apiClient } from "./httpClients"; - -export function guard(navigator, evaluator, redirect, navigateOptions, onRedirect) { - if (!evaluator) throw new Error("evaluator required."); - if (!redirect) throw new Error("redirect required."); - if (!navigateOptions) { - navigateOptions = { - replace: true - }; - } - let redirecting = !evaluator(); - if (redirecting) { - if (onRedirect) onRedirect(); - navigator(redirect, navigateOptions); - } -} - -export async function needUser(navigator) { - let userDataResponse = await apiClient.get("/user"); - guard(navigator, () => userDataResponse.status === 200, "/login"); - return userDataResponse.data; -} \ No newline at end of file diff --git a/sports-matcher/server/.eslintrc.json b/sports-matcher/server/.eslintrc.json index 0806fb5..e296e45 100644 --- a/sports-matcher/server/.eslintrc.json +++ b/sports-matcher/server/.eslintrc.json @@ -14,7 +14,7 @@ 4 ], "linebreak-style": [ - "error", + "warn", "unix" ], "quotes": [ @@ -24,6 +24,7 @@ "semi": [ "error", "always" - ] + ], + "no-unused-vars": "warn" } } \ No newline at end of file diff --git a/sports-matcher/server/controllers/matchController.js b/sports-matcher/server/controllers/matchController.js index 53cc013..6667744 100644 --- a/sports-matcher/server/controllers/matchController.js +++ b/sports-matcher/server/controllers/matchController.js @@ -1,5 +1,5 @@ import express from "express"; -import { requireAuthenticated } from "../middleware/authority.js"; +import { requireAdmin, requireAuthenticated } from "../middleware/authority.js"; import { needDatabase } from "../middleware/database.js"; import matchModel from "../schemas/matchModel.js"; import sportModel from "../schemas/sportModel.js"; @@ -27,17 +27,8 @@ MatchController.get("/search/:sport", needDatabase, async (req, res) => { MatchController.get("/recent/:limit?", needDatabase, async (req, res) => { try { - let user = null; - if (req.session.userId) { - user = await userModel.findById(req.session.userId); - } let limit = parseInt(req.params.limit); if (!req.params.limit) limit = 10; - if (isNaN(limit)) { - console.log(typeof (limit)); - res.status(400).send("Limit parameter is not a number."); - return; - } if (isNaN(limit)) { res.status(400).send("Limit parameter not a number."); return; @@ -46,18 +37,39 @@ MatchController.get("/recent/:limit?", needDatabase, async (req, res) => { res.status(400).send("Limit greater than maximum limit of 50."); return; } - let recent = null; - if (user) { - recent = matchModel.find({ creator: user._id }); - } else { - recent = matchModel.find().where("publicity").gte(2); - } + let recent = matchModel.find().where("publicity").gte(2); recent = await recent.sort({ createDate: -1 }).limit(limit).populate(["sport", "participants"]); res.status(200).send({ recent: recent }); } catch (error) { console.error(error); res.status(500).send("Internal server error."); - // TODO: Check and improve error handling. + } +}); + +MatchController.get("/all", requireAdmin, async (req, res) => { + try { + const allmatches = await matchModel.find().populate("sport"); + res.status(200).send({ all: allmatches }); + } catch (error) { + console.error(error); + res.status(500).send("Internal server error."); + } +}); + +MatchController.get("/recent/user/:limit", needDatabase, requireAuthenticated, async (req, res) => { + try { + let user = req.user; + let limit = parseInt(req.params.limit); + if (!req.params.limit) limit = 10; + if (isNaN(limit)) { + res.status(400).send("Limit parameter not a number."); + return; + } + let recent = await matchModel.find({ creator: user._id }).sort({ createDate: -1 }).limit(limit).populate(["sport", "participants"]); + res.status(200).send({ recent: recent }); + } catch (error) { + console.error(error); + res.status(500).send("Internal server error."); } }); diff --git a/sports-matcher/server/controllers/rentalController.js b/sports-matcher/server/controllers/rentalController.js index 616c09b..b1b55c0 100644 --- a/sports-matcher/server/controllers/rentalController.js +++ b/sports-matcher/server/controllers/rentalController.js @@ -39,7 +39,6 @@ rentalController.get("/recent/:limit?", needDatabase, async (req, res) => { let limit = parseInt(req.params.limit); if (!req.params.limit) limit = 10; if (isNaN(limit)) { - console.log(typeof (limit)); res.status(400).send("Limit parameter is not a number."); return; } diff --git a/sports-matcher/server/controllers/userController.js b/sports-matcher/server/controllers/userController.js index 520b788..47029b3 100644 --- a/sports-matcher/server/controllers/userController.js +++ b/sports-matcher/server/controllers/userController.js @@ -1,5 +1,5 @@ import express from "express"; -import { requireAuthenticated } from "../middleware/authority.js"; +import { requireAdmin, requireAuthenticated } from "../middleware/authority.js"; import { needDatabase } from "../middleware/database.js"; import userModel from "../schemas/userModel.js"; import User from "../schemas/userModel.js"; @@ -16,7 +16,8 @@ UserController.post("/login", needDatabase, async (req, res) => { } else { req.session.userId = user._id; req.session.email = user.email; - res.status(200).send("Authenticated."); + user.password = undefined; + res.status(200).send(user); } } catch (error) { if (error.name === "TypeError") { @@ -66,50 +67,91 @@ UserController.get("/:id?", needDatabase, requireAuthenticated, async (req, res) res.status(200).send(user); }); + UserController.patch("/:id?", needDatabase, requireAuthenticated, async (req, res) => { - let user = null; - if (req.params.id) { - if (req.user.accessLevel > 2) { - user = await userModel.findById(req.params.id); + try { + let user = null; + if (req.params.id) { + if (req.user.accessLevel > 2) { + user = await userModel.findById(req.params.id); + } else { + res.status(401).send("Unauthorized."); + return; + } } else { - res.status(401).send("Unauthorized."); + user = req.user; + } + if (req.body._id) { + res.status(400).send("Cannot change user ID."); return; } - } else { - user = req.user; - } - if (req.body._id) { - res.status(400).send("Cannot change user ID."); - return; - } - if (req.body.createdMatches) { - res.status(400).send("Cannot directly change the list of created matches."); - return; - } + if (req.body.createdMatches) { + res.status(400).send("Cannot directly change the list of created matches."); + return; + } - if (req.body.password) { - res.status(400).send("Cannot directly change user password."); - return; - } + if (req.body.password) { + res.status(400).send("Cannot directly change user password."); + return; + } - if (req.body.participatingMatches) { - res.status(400).send("Cannot directly change the list of participating matches."); - return; - } + if (req.body.participatingMatches) { + res.status(400).send("Cannot directly change the list of participating matches."); + return; + } - if (req.body.joinDate) { - res.status(400).send("Cannot change the join date."); - return; - } + if (req.body.joinDate) { + res.status(400).send("Cannot change the join date."); + return; + } - if (req.body.accessLevel && req.user.accessLevel < 3) { - res.status(401).send("Unauthorized to change the access level of this user."); - return; - } + if (req.body.accessLevel && req.user.accessLevel < 3) { + res.status(401).send("Unauthorized to change the access level of this user."); + return; + } - await user.updateOne(req.body); - res.status(200).send("Updated."); + if (req.body.suspend && req.user.accessLevel < 3) { + res.status(401).send("Unauthorized to change the accounts disabled date. "); + return; + } + + await user.updateOne(req.body); + res.status(200).send("Updated."); + } catch (error) { + console.error(error); + res.status(500).send("Internal server error"); + } +}); + +UserController.get("/all", requireAdmin, async (req, res) => { + try { + let all = await userModel.find(); + res.status(200).send({ all: all }); + } catch (error) { + console.error(error); + res.status(500).send("Internal server error"); + } +}); + +UserController.get("/all/active", requireAdmin, async (req, res) => { + try { + let active = await userModel.find().where("suspend").lt(Date.now()); + res.status(200).send({ active: active }); + } catch (error) { + console.error(error); + res.status(500).send("Internal server error"); + } +}); + +UserController.get("/all/suspended", requireAuthenticated, async (req, res) => { + try { + let suspended = await userModel.find().where("suspend").gte(Date.now()); + res.status(200).send({ suspended: suspended }); + } catch (error) { + console.error(error); + res.status(500).send("Internal server error"); + } }); /* TODO: Implement middleware for removing users. @@ -135,13 +177,15 @@ UserController.delete("/:id?", needDatabase, requireAuthenticated, async (req, r UserController.post("/", needDatabase, async (req, res) => { try { - let createdUser = new User({ + const data = { email: req.body.email, firstName: req.body.firstName, lastName: req.body.lastName, phone: req.body.phone, password: req.body.password, - }); + }; + + let createdUser = new User(data); await createdUser.save(); res.sendStatus(201); return; diff --git a/sports-matcher/server/middleware/authority.js b/sports-matcher/server/middleware/authority.js index 9c09f66..f7ff1b5 100644 --- a/sports-matcher/server/middleware/authority.js +++ b/sports-matcher/server/middleware/authority.js @@ -2,6 +2,7 @@ import MongoStore from "connect-mongo"; import session from "express-session"; import { mongooseDbName, mongoURI } from "../database/mongoose.js"; import userModel from "../schemas/userModel.js"; +import { checkDatabaseConnection } from "./database.js"; const sessionConf = { secret: process.env.SESSION_SECRET || "super duper secret string.", cookie: { @@ -18,6 +19,10 @@ if (process.env.NODE_ENV === "production") { export const userSession = session(sessionConf); export async function requireAuthenticated(req, res, next) { + if (!checkDatabaseConnection()) { + req.status(500).send("Internal server error."); + return; + } if (req.session.userId) { req.user = await userModel.findById(req.session.userId); next(); @@ -26,3 +31,22 @@ export async function requireAuthenticated(req, res, next) { return; } } + + +export async function requireAdmin(req, res, next) { + if (!checkDatabaseConnection()) { + req.status(500).send("Internal server error."); + return; + } + if (req.session.userId) { + req.user = await userModel.findById(req.session.userId); + if (req.user.accessLevel < 3) { + res.status(401).send("Not authorized"); + return; + } + next(); + } else { + res.status(401).send("Not authorized."); + return; + } +} \ No newline at end of file diff --git a/sports-matcher/server/middleware/database.js b/sports-matcher/server/middleware/database.js index 149344a..08cdf68 100644 --- a/sports-matcher/server/middleware/database.js +++ b/sports-matcher/server/middleware/database.js @@ -1,9 +1,13 @@ import mongoose from "mongoose"; -export function needDatabase(res, req, next) { - if (mongoose.connection.readyState != 1) { +export function needDatabase(req, res, next) { + if (!checkDatabaseConnection()) { res.status(500).send("Internal server error: Database connection faulty."); } else { next(); } +} + +export function checkDatabaseConnection() { + return mongoose.connection.readyState == 1; } \ No newline at end of file diff --git a/sports-matcher/server/schemas/matchModel.js b/sports-matcher/server/schemas/matchModel.js index 85c55ee..0e0dae5 100644 --- a/sports-matcher/server/schemas/matchModel.js +++ b/sports-matcher/server/schemas/matchModel.js @@ -21,7 +21,7 @@ const matchSchema = new mongoose.Schema({ participants: { type: [{ type: Types.ObjectId, ref: ModelNameRegister.User }], required: true, default: [] }, difficulty: { type: Number, required: true }, sport: { type: Types.ObjectId, ref: ModelNameRegister.Sport }, - createDate: { type: Date, required: true, default: Date.now } + createDate: { type: Date, required: true, default: Date.now() } }); matchSchema.pre("remove", function (next) { diff --git a/sports-matcher/server/schemas/rentalModel.js b/sports-matcher/server/schemas/rentalModel.js index 31e2d24..409d059 100644 --- a/sports-matcher/server/schemas/rentalModel.js +++ b/sports-matcher/server/schemas/rentalModel.js @@ -8,7 +8,7 @@ const rentalSchema = new mongoose.Schema({ rate: { type: String, required: true, trim: true }, description: { type: String, required: true }, contact: { type: String, required: true }, - createDate: { type: Date, required: true, default: Date.now }, + createDate: { type: Date, required: true, default: Date.now() }, creator: { type: Types.ObjectId, ref: modelNameRegister.User } }); diff --git a/sports-matcher/server/schemas/userModel.js b/sports-matcher/server/schemas/userModel.js index 28a2be5..0bf9195 100644 --- a/sports-matcher/server/schemas/userModel.js +++ b/sports-matcher/server/schemas/userModel.js @@ -19,7 +19,7 @@ const userSchema = new mongoose.Schema({ }, firstName: { type: String, required: true, trim: true }, lastName: { type: String, required: true, trim: true }, - joinDate: { type: Date, default: Date.now, required: true }, + joinDate: { type: Date, default: Date.now(), required: true }, phone: { type: Number, required: false, min: 0 }, password: { type: String, @@ -36,6 +36,7 @@ const userSchema = new mongoose.Schema({ participatingMatchesPublicity: { type: Boolean, required: true, default: false }, friends: { type: Types.ObjectId, ref: modelNameRegister.User }, accessLevel: { type: Number, required: true, default: 0 }, + suspend: { type: Date, required: true, default: Date.now() } // suspend the user until the when the user was created. }); userSchema.statics.credentialsExist = async function (email, password) {