diff --git a/.env-model b/.env-model new file mode 100644 index 0000000000000000000000000000000000000000..6f4241fff681635d57ba88ec016f50949b1cf92c --- /dev/null +++ b/.env-model @@ -0,0 +1,11 @@ +MYSQL_USER=localhost +MYSQL_PASSWORD=root +MYSQL_DATABASE=student +MYSQL_HOST=localhost +MYSQL_ROOT_PASSWORD=root +MYSQL_HOST_IP=mysql + +REACT_APP_SERVER_PORT=8000 + +NODE_PATH=src +PMA_HOST=mysql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..b901d1fc371116bf65b24743d88845c679e40595 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/client/node_modules +/server/node_modules +.env \ No newline at end of file diff --git a/README.md b/README.md index 7a3135480539c027be66ff7f5fe35581fc9b91b9..1a1a970204bec8efb58dce9cb4171d158be66185 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,27 @@ -# STUDENT +# APPLICATION - STUDENT -Front Student Module for ERP Project 2021 +## Front Student Module pour Projet ERP 2021 -To Install Project : +#### Avant d'executer l'application : - in client/ and server/ run npm start - sudo npm install -g nodemon + Renommer le fichier .env-model en .env -To Start Project : +#### Executer l'application : - in client/ run npm start - in server/ run nodemon + docker-compose up + + +#### Une fois le build terminé et les services lancés : + +Le client (l'interface) : + + localhost:3000 + + +Le serveur (API) : + + localhost:8000/api + +Visualisation de base de données via PhpMyAdmin + + localhost:8080 diff --git a/client/.dockerignore b/client/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..b7dab5e9cbfea4afa2bc788899ccaea6df849062 --- /dev/null +++ b/client/.dockerignore @@ -0,0 +1,2 @@ +node_modules +build \ No newline at end of file diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e282c3f106a2c2dc5cea0929bad6c0f20df67b26 --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,10 @@ +FROM node:10-alpine + +RUN mkdir -p /app +WORKDIR /app + +COPY . . + +RUN npm install + +CMD ["npm", "start"] \ No newline at end of file diff --git a/client/package.json b/client/package.json index ba59707aa217ea3a41f40004459fe8e3eee24c1b..a4729a20057a287e633237d15b7adf70c3e86700 100644 --- a/client/package.json +++ b/client/package.json @@ -2,15 +2,21 @@ "name": "client", "version": "0.1.0", "private": true, + "proxy": "http://server:8000", "dependencies": { - "@testing-library/jest-dom": "^5.14.1", - "@testing-library/react": "^11.2.7", - "@testing-library/user-event": "^12.8.3", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-scripts": "4.0.3", - "web-vitals": "^1.1.2", - "bootstrap": "3.3.7" + "axios": "^0.19.0", + "bootstrap": "^5.1.1", + "cors": "^2.8.5", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-redux": "^7.2.1", + "react-router-dom": "^5.2.0", + "react-scripts": "3.0.1", + "react-validation": "^3.0.7", + "redux": "^4.1.1", + "redux-thunk": "^2.3.0", + "validator": "^13.6.0", + "webpack": "4.29.6" }, "scripts": { "start": "react-scripts start", @@ -19,10 +25,7 @@ "eject": "react-scripts eject" }, "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] + "extends": "react-app" }, "browserslist": { "production": [ @@ -36,5 +39,7 @@ "last 1 safari version" ] }, - "proxy": "http://localhost:3001" + "devDependencies": { + "redux-devtools-extension": "^2.13.9" + } } diff --git a/client/public/favicon.ico b/client/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a Binary files /dev/null and b/client/public/favicon.ico differ diff --git a/client/public/index.html b/client/public/index.html new file mode 100644 index 0000000000000000000000000000000000000000..dd1ccfd4cd30a29aaa08b295d99be29cdeb29cf9 --- /dev/null +++ b/client/public/index.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="theme-color" content="#000000" /> + <!-- + manifest.json provides metadata used when your web app is installed on a + user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ + --> + <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> + <!-- + Notice the use of %PUBLIC_URL% in the tags above. + It will be replaced with the URL of the `public` folder during the build. + Only files inside the `public` folder can be referenced from the HTML. + + Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will + work correctly both with client-side routing and a non-root public URL. + Learn how to configure a non-root public URL by running `npm run build`. + --> + <title>React App</title> + </head> + <body> + <noscript>You need to enable JavaScript to run this app.</noscript> + <div id="root"></div> + <!-- + This HTML file is a template. + If you open it directly in the browser, you will see an empty page. + + You can add webfonts, meta tags, or analytics to this file. + The build step will place the bundled scripts into the <body> tag. + + To begin the development, run `npm start` or `yarn start`. + To create a production bundle, use `npm run build` or `yarn build`. + --> + </body> +</html> diff --git a/client/public/manifest.json b/client/public/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..1f2f141fafdeb1d31d85b008ec5132840c5e6362 --- /dev/null +++ b/client/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/client/src/App.css b/client/src/App.css deleted file mode 100644 index 6b027ba19972697f3baafec248da6e0033cd082c..0000000000000000000000000000000000000000 --- a/client/src/App.css +++ /dev/null @@ -1,18 +0,0 @@ -body { background: #f7f7f7; } -.app { margin: 3em; width: 24em; } - -input, -button { - padding: 0.5em 0.75em; - margin-right: 1em; - border-radius: 4px; - outline: none; - border: 1px solid transparent; - font: inherit; -} - -input { border-color: #888; } -input:focus { border-color: #17f; } - -button { background: #17f; color: #fff; } -button:active { background: #15d; } \ No newline at end of file diff --git a/client/src/App.js b/client/src/App.js deleted file mode 100644 index 8668b21a1c4067a331005fa02a991f378582a4db..0000000000000000000000000000000000000000 --- a/client/src/App.js +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useState } from "react"; - -function App() { - const [word, setWord] = React.useState('software'); - const [associations, setAssociations] = React.useState(null); - - const getAssociations = () => { - fetch('/api/' + word) - .then(result => result.json()) - .then(body => setAssociations(body)); - }; - - return ( - <div className="app"> - <h1>Word Associations Map</h1> - <input value={word} onChange={e => setWord(e.target.value)} /> - <button onClick={getAssociations}>Find Associations</button> - {associations && ( - Object.keys(associations).length === 0 - ? <p>No results</p> - : <div> - {Object.entries(associations).map(([association, score]) => ( - <span style={{ fontSize: Math.pow(score, 2) / 200 }}> - {association + ' => ' + score} - {' '} - </span> - ))} - </div> - )} - </div> - ); -} - -export default App; \ No newline at end of file diff --git a/client/src/App.test.js b/client/src/App.test.js deleted file mode 100644 index 1f03afeece5ac28064fa3c73a29215037465f789..0000000000000000000000000000000000000000 --- a/client/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(<App />); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/client/src/actions/auth.js b/client/src/actions/auth.js new file mode 100644 index 0000000000000000000000000000000000000000..33ed2c0660009c180f1eace1e5706d2a905734cc --- /dev/null +++ b/client/src/actions/auth.js @@ -0,0 +1,87 @@ +import { + REGISTER_SUCCESS, + REGISTER_FAIL, + LOGIN_SUCCESS, + LOGIN_FAIL, + LOGOUT, + SET_MESSAGE, + } from "./types"; + + import AuthService from "../services/auth.service"; + + export const register = (username, email, password) => (dispatch) => { + return AuthService.register(username, email, password).then( + (response) => { + dispatch({ + type: REGISTER_SUCCESS, + }); + + dispatch({ + type: SET_MESSAGE, + payload: response.data.message, + }); + + return Promise.resolve(); + }, + (error) => { + const message = + (error.response && + error.response.data && + error.response.data.message) || + error.message || + error.toString(); + + dispatch({ + type: REGISTER_FAIL, + }); + + dispatch({ + type: SET_MESSAGE, + payload: message, + }); + + return Promise.reject(); + } + ); + }; + + export const login = (username, password) => (dispatch) => { + return AuthService.login(username, password).then( + (data) => { + dispatch({ + type: LOGIN_SUCCESS, + payload: { user: data }, + }); + + return Promise.resolve(); + }, + (error) => { + const message = + (error.response && + error.response.data && + error.response.data.message) || + error.message || + error.toString(); + + dispatch({ + type: LOGIN_FAIL, + }); + + dispatch({ + type: SET_MESSAGE, + payload: message, + }); + + return Promise.reject(); + } + ); + }; + + export const logout = () => (dispatch) => { + AuthService.logout(); + + dispatch({ + type: LOGOUT, + }); + }; + \ No newline at end of file diff --git a/client/src/actions/message.js b/client/src/actions/message.js new file mode 100644 index 0000000000000000000000000000000000000000..9aba65774019ee025f6223c4698c116661f8162f --- /dev/null +++ b/client/src/actions/message.js @@ -0,0 +1,10 @@ +import { SET_MESSAGE, CLEAR_MESSAGE } from "./types"; + +export const setMessage = (message) => ({ + type: SET_MESSAGE, + payload: message, +}); + +export const clearMessage = () => ({ + type: CLEAR_MESSAGE, +}); diff --git a/client/src/actions/types.js b/client/src/actions/types.js new file mode 100644 index 0000000000000000000000000000000000000000..b34faca5da7fd2ecbafc70b40a5a88db7562b40e --- /dev/null +++ b/client/src/actions/types.js @@ -0,0 +1,8 @@ +export const REGISTER_SUCCESS = "REGISTER_SUCCESS"; +export const REGISTER_FAIL = "REGISTER_FAIL"; +export const LOGIN_SUCCESS = "LOGIN_SUCCESS"; +export const LOGIN_FAIL = "LOGIN_FAIL"; +export const LOGOUT = "LOGOUT"; + +export const SET_MESSAGE = "SET_MESSAGE"; +export const CLEAR_MESSAGE = "CLEAR_MESSAGE"; diff --git a/client/src/app.css b/client/src/app.css new file mode 100644 index 0000000000000000000000000000000000000000..a7b0abfe88aa5553d10be565646a913b65300de7 --- /dev/null +++ b/client/src/app.css @@ -0,0 +1,33 @@ +label { + display: block; + margin-top: 10px; + } + + .card-container.card { + max-width: 350px !important; + padding: 40px 40px; + } + + .card { + background-color: #f7f7f7; + padding: 20px 25px 30px; + margin: 0 auto 25px; + margin-top: 50px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; + -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); + -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); + box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); + } + + .profile-img-card { + width: 96px; + height: 96px; + margin: 0 auto 10px; + display: block; + -moz-border-radius: 50%; + -webkit-border-radius: 50%; + border-radius: 50%; + } + \ No newline at end of file diff --git a/client/src/app.js b/client/src/app.js new file mode 100644 index 0000000000000000000000000000000000000000..d0fc494125f90a819c5d8e21c784e12c1467a051 --- /dev/null +++ b/client/src/app.js @@ -0,0 +1,149 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { Router, Switch, Route, Link } from "react-router-dom"; + +import "bootstrap/dist/css/bootstrap.min.css"; +import "./app.css"; + +import Login from "./components/login.component"; +import Register from "./components/register.component"; +import Home from "./components/home.component"; +import Profile from "./components/profile.component"; +import BoardUser from "./components/board-user.component"; +import BoardModerator from "./components/board-moderator.component"; +import BoardAdmin from "./components/board-admin.component"; + +import { logout } from "./actions/auth"; +import { clearMessage } from "./actions/message"; + +import { history } from './helpers/history'; + +class App extends Component { + constructor(props) { + super(props); + this.logOut = this.logOut.bind(this); + + this.state = { + showModeratorBoard: false, + showAdminBoard: false, + currentUser: undefined, + }; + + history.listen((location) => { + props.dispatch(clearMessage()); // clear message when changing location + }); + } + + componentDidMount() { + const user = this.props.user; + + if (user) { + this.setState({ + currentUser: user, + showModeratorBoard: user.roles.includes("ROLE_MODERATOR"), + showAdminBoard: user.roles.includes("ROLE_ADMIN"), + }); + } + } + + logOut() { + this.props.dispatch(logout()); + } + + render() { + const { currentUser, showModeratorBoard, showAdminBoard } = this.state; + + return ( + <Router history={history}> + <div> + <nav className="navbar navbar-expand navbar-dark bg-dark"> + <Link to={"/"} className="navbar-brand"> + STUDENT + </Link> + <div className="navbar-nav mr-auto"> + <li className="nav-item"> + <Link to={"/home"} className="nav-link"> + Home + </Link> + </li> + + {showModeratorBoard && ( + <li className="nav-item"> + <Link to={"/mod"} className="nav-link"> + Moderator Board + </Link> + </li> + )} + + {showAdminBoard && ( + <li className="nav-item"> + <Link to={"/admin"} className="nav-link"> + Admin Board + </Link> + </li> + )} + + {currentUser && ( + <li className="nav-item"> + <Link to={"/user"} className="nav-link"> + User + </Link> + </li> + )} + </div> + + {currentUser ? ( + <div className="navbar-nav ml-auto"> + <li className="nav-item"> + <Link to={"/profile"} className="nav-link"> + {currentUser.username} + </Link> + </li> + <li className="nav-item"> + <a href="/login" className="nav-link" onClick={this.logOut}> + LogOut + </a> + </li> + </div> + ) : ( + <div className="navbar-nav ml-auto"> + <li className="nav-item"> + <Link to={"/login"} className="nav-link"> + Login + </Link> + </li> + + <li className="nav-item"> + <Link to={"/register"} className="nav-link"> + Sign Up + </Link> + </li> + </div> + )} + </nav> + + <div className="container mt-3"> + <Switch> + <Route exact path={["/", "/home"]} component={Home} /> + <Route exact path="/login" component={Login} /> + <Route exact path="/register" component={Register} /> + <Route exact path="/profile" component={Profile} /> + <Route path="/user" component={BoardUser} /> + <Route path="/mod" component={BoardModerator} /> + <Route path="/admin" component={BoardAdmin} /> + </Switch> + </div> + </div> + </Router> + ); + } +} + +function mapStateToProps(state) { + const { user } = state.auth; + return { + user, + }; +} + +export default connect(mapStateToProps)(App); diff --git a/client/src/components/board-admin.component.js b/client/src/components/board-admin.component.js new file mode 100644 index 0000000000000000000000000000000000000000..6093a64899b4421b3b019132b74bb37b3e36b488 --- /dev/null +++ b/client/src/components/board-admin.component.js @@ -0,0 +1,43 @@ +import React, { Component } from "react"; + +import UserService from "../services/user.service"; + +export default class BoardUser extends Component { + constructor(props) { + super(props); + + this.state = { + content: "" + }; + } + + componentDidMount() { + UserService.getUserBoard().then( + response => { + this.setState({ + content: response.data + }); + }, + error => { + this.setState({ + content: + (error.response && + error.response.data && + error.response.data.message) || + error.message || + error.toString() + }); + } + ); + } + + render() { + return ( + <div className="container"> + <header className="jumbotron"> + <h3>{this.state.content}</h3> + </header> + </div> + ); + } +} diff --git a/client/src/components/board-moderator.component.js b/client/src/components/board-moderator.component.js new file mode 100644 index 0000000000000000000000000000000000000000..4ac60f3c097b97cb62fc76f859da04ec6ef64702 --- /dev/null +++ b/client/src/components/board-moderator.component.js @@ -0,0 +1,43 @@ +import React, { Component } from "react"; + +import UserService from "../services/user.service"; + +export default class BoardUser extends Component { + constructor(props) { + super(props); + + this.state = { + content: "" + }; + } + + componentDidMount() { + UserService.getModeratorBoard().then( + response => { + this.setState({ + content: response.data + }); + }, + error => { + this.setState({ + content: + (error.response && + error.response.data && + error.response.data.message) || + error.message || + error.toString() + }); + } + ); + } + + render() { + return ( + <div className="container"> + <header className="jumbotron"> + <h3>{this.state.content}</h3> + </header> + </div> + ); + } +} diff --git a/client/src/components/board-user.component.js b/client/src/components/board-user.component.js new file mode 100644 index 0000000000000000000000000000000000000000..6093a64899b4421b3b019132b74bb37b3e36b488 --- /dev/null +++ b/client/src/components/board-user.component.js @@ -0,0 +1,43 @@ +import React, { Component } from "react"; + +import UserService from "../services/user.service"; + +export default class BoardUser extends Component { + constructor(props) { + super(props); + + this.state = { + content: "" + }; + } + + componentDidMount() { + UserService.getUserBoard().then( + response => { + this.setState({ + content: response.data + }); + }, + error => { + this.setState({ + content: + (error.response && + error.response.data && + error.response.data.message) || + error.message || + error.toString() + }); + } + ); + } + + render() { + return ( + <div className="container"> + <header className="jumbotron"> + <h3>{this.state.content}</h3> + </header> + </div> + ); + } +} diff --git a/client/src/components/home.component.js b/client/src/components/home.component.js new file mode 100644 index 0000000000000000000000000000000000000000..13d435ed70f023b210149c6290175134bde56743 --- /dev/null +++ b/client/src/components/home.component.js @@ -0,0 +1,84 @@ +import React, { Component } from "react"; + +import UserService from "../services/user.service"; + +export default class Home extends Component { + constructor(props) { + super(props); + + this.state = { + content: "" + }; + } + + componentDidMount() { + UserService.getPublicContent().then( + response => { + this.setState({ + content: response.data + }); + }, + error => { + this.setState({ + content: + (error.response && error.response.data) || + error.message || + error.toString() + }); + } + ); + } + + getFormattedDate(date) { + var timestamp = Date.parse(date); + var formatDate = new Date(timestamp); + var dd = String(formatDate.getDate()).padStart(2, '0'); + var mm = String(formatDate.getMonth() + 1).padStart(2, '0'); + var yyyy = formatDate.getFullYear(); + + return dd + '/' + mm + '/' + yyyy; +} + + + render() { + return ( + <div className="container"> + <header className="jumbotron"> + <h3>Welcome</h3> + </header> + <div> + { this.state.content ? + <table className="table"> + <thead> + <tr> + <th scope="col">#</th> + <th scope="col">Nom utilisateur</th> + <th scope="col">Nom</th> + <th scope="col">Prenom</th> + <th scope="col">Email</th> + <th scope="col">Date de naissance</th> + <th scope="col">Numero étudiant</th> + <th scope="col">Date d'adhésion</th> + </tr> + </thead> + <tbody> + {Object.entries(this.state.content).map(([key, student]) => ( + <tr key={key}> + <th scope="row">{student.id}</th> + <td>{student.nom_utilisateur}</td> + <td>{student.nom}</td> + <td>{student.prenom}</td> + <td>{student.email}</td> + <td>{this.getFormattedDate(student.date_naissance)}</td> + <td>{student.numero_etudiant}</td> + <td>{this.getFormattedDate(student.date_adhesion)}</td> + </tr> + ))} + </tbody> + </table> + : "" } + </div> + </div> + ); + } +} diff --git a/client/src/components/index.js b/client/src/components/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bae61df19ea99cd8794ddedb75ef3e2761030b0d --- /dev/null +++ b/client/src/components/index.js @@ -0,0 +1 @@ +export { SampleComponent } from './sample-component/sample-component'; \ No newline at end of file diff --git a/client/src/components/login.component.js b/client/src/components/login.component.js new file mode 100644 index 0000000000000000000000000000000000000000..ea3603ad675723af9d8cdd5ad7c4c09b4f40864c --- /dev/null +++ b/client/src/components/login.component.js @@ -0,0 +1,163 @@ +import React, { Component } from "react"; +import { Redirect } from 'react-router-dom'; + +import Form from "react-validation/build/form"; +import Input from "react-validation/build/input"; +import CheckButton from "react-validation/build/button"; + +import { connect } from "react-redux"; +import { login } from "../actions/auth"; + +const required = (value) => { + if (!value) { + return ( + <div className="alert alert-danger" role="alert"> + This field is required! + </div> + ); + } +}; + +class Login extends Component { + constructor(props) { + super(props); + this.handleLogin = this.handleLogin.bind(this); + this.onChangeUsername = this.onChangeUsername.bind(this); + this.onChangePassword = this.onChangePassword.bind(this); + + this.state = { + username: "", + password: "", + loading: false, + }; + } + + onChangeUsername(e) { + this.setState({ + username: e.target.value, + }); + } + + onChangePassword(e) { + this.setState({ + password: e.target.value, + }); + } + + handleLogin(e) { + e.preventDefault(); + + this.setState({ + loading: true, + }); + + this.form.validateAll(); + + const { dispatch, history } = this.props; + + if (this.checkBtn.context._errors.length === 0) { + dispatch(login(this.state.username, this.state.password)) + .then(() => { + history.push("/profile"); + window.location.reload(); + }) + .catch(() => { + this.setState({ + loading: false + }); + }); + } else { + this.setState({ + loading: false, + }); + } + } + + render() { + const { isLoggedIn, message } = this.props; + + if (isLoggedIn) { + return <Redirect to="/profile" />; + } + + return ( + <div className="col-md-12"> + <div className="card card-container"> + <img + src="//ssl.gstatic.com/accounts/ui/avatar_2x.png" + alt="profile-img" + className="profile-img-card" + /> + + <Form + onSubmit={this.handleLogin} + ref={(c) => { + this.form = c; + }} + > + <div className="form-group"> + <label htmlFor="username">Username</label> + <Input + type="text" + className="form-control" + name="username" + value={this.state.username} + onChange={this.onChangeUsername} + validations={[required]} + /> + </div> + + <div className="form-group"> + <label htmlFor="password">Password</label> + <Input + type="password" + className="form-control" + name="password" + value={this.state.password} + onChange={this.onChangePassword} + validations={[required]} + /> + </div> + + <div className="form-group"> + <button + className="btn btn-primary btn-block" + disabled={this.state.loading} + > + {this.state.loading && ( + <span className="spinner-border spinner-border-sm"></span> + )} + <span>Login</span> + </button> + </div> + + {message && ( + <div className="form-group"> + <div className="alert alert-danger" role="alert"> + {message} + </div> + </div> + )} + <CheckButton + style={{ display: "none" }} + ref={(c) => { + this.checkBtn = c; + }} + /> + </Form> + </div> + </div> + ); + } +} + +function mapStateToProps(state) { + const { isLoggedIn } = state.auth; + const { message } = state.message; + return { + isLoggedIn, + message + }; +} + +export default connect(mapStateToProps)(Login); diff --git a/client/src/components/profile.component.js b/client/src/components/profile.component.js new file mode 100644 index 0000000000000000000000000000000000000000..2265696a19af38fb4d4ddc6d4577b6bbe224cb53 --- /dev/null +++ b/client/src/components/profile.component.js @@ -0,0 +1,48 @@ +import React, { Component } from "react"; +import { Redirect } from 'react-router-dom'; +import { connect } from "react-redux"; + +class Profile extends Component { + + render() { + const { user: currentUser } = this.props; + + if (!currentUser) { + return <Redirect to="/login" />; + } + + return ( + <div className="container"> + <header className="jumbotron"> + <h3> + <strong>{currentUser.username}</strong> Profile + </h3> + </header> + <p> + <strong>Token:</strong> {currentUser.accessToken.substring(0, 20)} ...{" "} + {currentUser.accessToken.substr(currentUser.accessToken.length - 20)} + </p> + <p> + <strong>Id:</strong> {currentUser.id} + </p> + <p> + <strong>Email:</strong> {currentUser.email} + </p> + <strong>Authorities:</strong> + <ul> + {currentUser.roles && + currentUser.roles.map((role, index) => <li key={index}>{role}</li>)} + </ul> + </div> + ); + } +} + +function mapStateToProps(state) { + const { user } = state.auth; + return { + user, + }; +} + +export default connect(mapStateToProps)(Profile); diff --git a/client/src/components/register.component.js b/client/src/components/register.component.js new file mode 100644 index 0000000000000000000000000000000000000000..042e63b1ca1c102e90807c87816881e993e3584d --- /dev/null +++ b/client/src/components/register.component.js @@ -0,0 +1,200 @@ +import React, { Component } from "react"; +import Form from "react-validation/build/form"; +import Input from "react-validation/build/input"; +import CheckButton from "react-validation/build/button"; +import { isEmail } from "validator"; + +import { connect } from "react-redux"; +import { register } from "../actions/auth"; + +const required = (value) => { + if (!value) { + return ( + <div className="alert alert-danger" role="alert"> + This field is required! + </div> + ); + } +}; + +const email = (value) => { + if (!isEmail(value)) { + return ( + <div className="alert alert-danger" role="alert"> + This is not a valid email. + </div> + ); + } +}; + +const vusername = (value) => { + if (value.length < 3 || value.length > 20) { + return ( + <div className="alert alert-danger" role="alert"> + The username must be between 3 and 20 characters. + </div> + ); + } +}; + +const vpassword = (value) => { + if (value.length < 6 || value.length > 40) { + return ( + <div className="alert alert-danger" role="alert"> + The password must be between 6 and 40 characters. + </div> + ); + } +}; + +class Register extends Component { + constructor(props) { + super(props); + this.handleRegister = this.handleRegister.bind(this); + this.onChangeUsername = this.onChangeUsername.bind(this); + this.onChangeEmail = this.onChangeEmail.bind(this); + this.onChangePassword = this.onChangePassword.bind(this); + + this.state = { + username: "", + email: "", + password: "", + successful: false, + }; + } + + onChangeUsername(e) { + this.setState({ + username: e.target.value, + }); + } + + onChangeEmail(e) { + this.setState({ + email: e.target.value, + }); + } + + onChangePassword(e) { + this.setState({ + password: e.target.value, + }); + } + + handleRegister(e) { + e.preventDefault(); + + this.setState({ + successful: false, + }); + + this.form.validateAll(); + + if (this.checkBtn.context._errors.length === 0) { + this.props + .dispatch( + register(this.state.username, this.state.email, this.state.password) + ) + .then(() => { + this.setState({ + successful: true, + }); + }) + .catch(() => { + this.setState({ + successful: false, + }); + }); + } + } + + render() { + const { message } = this.props; + + return ( + <div className="col-md-12"> + <div className="card card-container"> + <img + src="//ssl.gstatic.com/accounts/ui/avatar_2x.png" + alt="profile-img" + className="profile-img-card" + /> + + <Form + onSubmit={this.handleRegister} + ref={(c) => { + this.form = c; + }} + > + {!this.state.successful && ( + <div> + <div className="form-group"> + <label htmlFor="username">Username</label> + <Input + type="text" + className="form-control" + name="username" + value={this.state.username} + onChange={this.onChangeUsername} + validations={[required, vusername]} + /> + </div> + + <div className="form-group"> + <label htmlFor="email">Email</label> + <Input + type="text" + className="form-control" + name="email" + value={this.state.email} + onChange={this.onChangeEmail} + validations={[required, email]} + /> + </div> + + <div className="form-group"> + <label htmlFor="password">Password</label> + <Input + type="password" + className="form-control" + name="password" + value={this.state.password} + onChange={this.onChangePassword} + validations={[required, vpassword]} + /> + </div> + + <div className="form-group"> + <button className="btn btn-primary btn-block">Sign Up</button> + </div> + </div> + )} + + {message && ( + <div className="form-group"> + <div className={ this.state.successful ? "alert alert-success" : "alert alert-danger" } role="alert"> + {message} + </div> + </div> + )} + <CheckButton + style={{ display: "none" }} + ref={(c) => { + this.checkBtn = c; + }} + /> + </Form> + </div> + </div> + ); + } +} + +function mapStateToProps(state) { + const { message } = state.message; + return { + message, + }; +} + +export default connect(mapStateToProps)(Register); diff --git a/client/src/components/sample-component/sample-component.jsx b/client/src/components/sample-component/sample-component.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d8a3035eea72a02ca8efc4246de68d4a061d3c03 --- /dev/null +++ b/client/src/components/sample-component/sample-component.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import axios from 'axios'; + +function callServer() { + axios.get(`http://localhost:${process.env.REACT_APP_SERVER_PORT}/test`, { + params: { + table: 'sample', + }, + }).then((response) => { + console.log(response.data); + }); +} + +export function SampleComponent() { + return ( + <div> + This is a sample component + <table id='students'> + <tbody> + {callServer()} + </tbody> + </table> + </div> + ); +} \ No newline at end of file diff --git a/client/src/helpers/history.js b/client/src/helpers/history.js new file mode 100644 index 0000000000000000000000000000000000000000..4c002e03aa6d7033cf1dfca3175cfacc0e38755b --- /dev/null +++ b/client/src/helpers/history.js @@ -0,0 +1,3 @@ +import { createBrowserHistory } from "history"; + +export const history = createBrowserHistory(); diff --git a/client/src/index.css b/client/src/index.css deleted file mode 100644 index 6b027ba19972697f3baafec248da6e0033cd082c..0000000000000000000000000000000000000000 --- a/client/src/index.css +++ /dev/null @@ -1,18 +0,0 @@ -body { background: #f7f7f7; } -.app { margin: 3em; width: 24em; } - -input, -button { - padding: 0.5em 0.75em; - margin-right: 1em; - border-radius: 4px; - outline: none; - border: 1px solid transparent; - font: inherit; -} - -input { border-color: #888; } -input:focus { border-color: #17f; } - -button { background: #17f; color: #fff; } -button:active { background: #15d; } \ No newline at end of file diff --git a/client/src/index.js b/client/src/index.js index ef2edf8ea3fc42258464231e29140c8723458c1e..b63b6509ff7de50978efa95de4442c678f97791c 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -1,17 +1,21 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +import App from './app'; +import * as serviceWorker from './serviceWorker'; +import { Provider } from 'react-redux'; +import store from './store'; + ReactDOM.render( - <React.StrictMode> - <App /> - </React.StrictMode>, + + <Provider store={store}> + <App /> + </Provider> +, document.getElementById('root') ); -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); +// If you want your app to work offline and load faster, you can change +// unregister() to register() below. Note this comes with some pitfalls. +// Learn more about service workers: https://bit.ly/CRA-PWA +serviceWorker.unregister(); diff --git a/client/src/logo.svg b/client/src/logo.svg deleted file mode 100644 index 9dfc1c058cebbef8b891c5062be6f31033d7d186..0000000000000000000000000000000000000000 --- a/client/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg> \ No newline at end of file diff --git a/client/src/reducers/auth.js b/client/src/reducers/auth.js new file mode 100644 index 0000000000000000000000000000000000000000..39b7fcf047e7bb10eb2ad094eeb638916d3beefd --- /dev/null +++ b/client/src/reducers/auth.js @@ -0,0 +1,51 @@ +import { + REGISTER_SUCCESS, + REGISTER_FAIL, + LOGIN_SUCCESS, + LOGIN_FAIL, + LOGOUT, + } from "../actions/types"; + + const user = JSON.parse(localStorage.getItem("user")); + + const initialState = user + ? { isLoggedIn: true, user } + : { isLoggedIn: false, user: null }; + + export default function (state = initialState, action) { + const { type, payload } = action; + + switch (type) { + case REGISTER_SUCCESS: + return { + ...state, + isLoggedIn: false, + }; + case REGISTER_FAIL: + return { + ...state, + isLoggedIn: false, + }; + case LOGIN_SUCCESS: + return { + ...state, + isLoggedIn: true, + user: payload.user, + }; + case LOGIN_FAIL: + return { + ...state, + isLoggedIn: false, + user: null, + }; + case LOGOUT: + return { + ...state, + isLoggedIn: false, + user: null, + }; + default: + return state; + } + } + \ No newline at end of file diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8cb74c9010357a25a646799a6e4c3b1d7a0891ae --- /dev/null +++ b/client/src/reducers/index.js @@ -0,0 +1,8 @@ +import { combineReducers } from "redux"; +import auth from "./auth"; +import message from "./message"; + +export default combineReducers({ + auth, + message, +}); diff --git a/client/src/reducers/message.js b/client/src/reducers/message.js new file mode 100644 index 0000000000000000000000000000000000000000..3740826b7f264af3ad938c47a69ee7b46e829af1 --- /dev/null +++ b/client/src/reducers/message.js @@ -0,0 +1,18 @@ +import { SET_MESSAGE, CLEAR_MESSAGE } from "../actions/types"; + +const initialState = {}; + +export default function (state = initialState, action) { + const { type, payload } = action; + + switch (type) { + case SET_MESSAGE: + return { message: payload }; + + case CLEAR_MESSAGE: + return { message: "" }; + + default: + return state; + } +} diff --git a/client/src/reportWebVitals.js b/client/src/reportWebVitals.js deleted file mode 100644 index 5253d3ad9e6be6690549cb255f5952337b02401d..0000000000000000000000000000000000000000 --- a/client/src/reportWebVitals.js +++ /dev/null @@ -1,13 +0,0 @@ -const reportWebVitals = onPerfEntry => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/client/src/serviceWorker.js b/client/src/serviceWorker.js new file mode 100644 index 0000000000000000000000000000000000000000..f8c7e50c201765c456ddbf21e9ea5b3e6a936920 --- /dev/null +++ b/client/src/serviceWorker.js @@ -0,0 +1,135 @@ +// This optional code is used to register a service worker. +// register() is not called by default. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on subsequent visits to a page, after all the +// existing tabs open on the page have been closed, since previously cached +// resources are updated in the background. + +// To learn more about the benefits of this model and instructions on how to +// opt-in, read https://bit.ly/CRA-PWA + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.1/8 is considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) +); + +export function register(config) { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://bit.ly/CRA-PWA' + ); + }); + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config); + } + }); + } +} + +function registerValidSW(swUrl, config) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + if (installingWorker == null) { + return; + } + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + 'New content is available and will be used when all ' + + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' + ); + + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration); + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.'); + + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration); + } + } + } + }; + }; + }) + .catch(error => { + console.error('Error during service worker registration:', error); + }); +} + +function checkValidServiceWorker(swUrl, config) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get('content-type'); + if ( + response.status === 404 || + (contentType != null && contentType.indexOf('javascript') === -1) + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config); + } + }) + .catch(() => { + console.log( + 'No internet connection found. App is running in offline mode.' + ); + }); +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready.then(registration => { + registration.unregister(); + }); + } +} diff --git a/client/src/services/auth-header.js b/client/src/services/auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..31f6d1a009ec65a4ff9573855e609f7f48c01e03 --- /dev/null +++ b/client/src/services/auth-header.js @@ -0,0 +1,10 @@ +export default function authHeader() { + const user = JSON.parse(localStorage.getItem('user')); + + if (user && user.accessToken) { + // for Node.js Express back-end + return { 'x-access-token': user.accessToken }; + } else { + return {}; + } +} \ No newline at end of file diff --git a/client/src/services/auth.service.js b/client/src/services/auth.service.js new file mode 100644 index 0000000000000000000000000000000000000000..98388a518f38d30f56259dc9b3862b286dc6009b --- /dev/null +++ b/client/src/services/auth.service.js @@ -0,0 +1,31 @@ +import axios from "axios"; + +const API_URL = "http://localhost:8000/api/auth/"; + +class AuthService { + login(username, password) { + return axios + .post(API_URL + "signin", { username, password }) + .then((response) => { + if (response.data.accessToken) { + localStorage.setItem("user", JSON.stringify(response.data)); + } + + return response.data; + }); + } + + logout() { + localStorage.removeItem("user"); + } + + register(username, email, password) { + return axios.post(API_URL + "signup", { + username, + email, + password, + }); + } +} + +export default new AuthService(); diff --git a/client/src/services/user.service.js b/client/src/services/user.service.js new file mode 100644 index 0000000000000000000000000000000000000000..2eaafcb6a52805e5485b4dd7b6266beade195a79 --- /dev/null +++ b/client/src/services/user.service.js @@ -0,0 +1,24 @@ +import axios from 'axios'; +import authHeader from './auth-header'; + +const API_URL = 'http://localhost:8000/api/auth/'; + +class UserService { +getPublicContent() { + return axios.get(API_URL + 'all'); +} + +getUserBoard() { + return axios.get(API_URL + 'user', { headers: authHeader() }); +} + +getModeratorBoard() { + return axios.get(API_URL + 'mod', { headers: authHeader() }); +} + +getAdminBoard() { + return axios.get(API_URL + 'admin', { headers: authHeader() }); +} +} + +export default new UserService(); diff --git a/client/src/setupTests.js b/client/src/setupTests.js deleted file mode 100644 index 8f2609b7b3e0e3897ab3bcaad13caf6876e48699..0000000000000000000000000000000000000000 --- a/client/src/setupTests.js +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; diff --git a/client/src/store.js b/client/src/store.js new file mode 100644 index 0000000000000000000000000000000000000000..53c4739b1f9be83df8b324c9f96617d91ed73185 --- /dev/null +++ b/client/src/store.js @@ -0,0 +1,13 @@ +import { createStore, applyMiddleware } from "redux"; +import { composeWithDevTools } from "redux-devtools-extension"; +import thunk from "redux-thunk"; +import rootReducer from "./reducers"; + +const middleware = [thunk]; + +const store = createStore( + rootReducer, + composeWithDevTools(applyMiddleware(...middleware)) +); + +export default store; diff --git a/client/webpack.config.js b/client/webpack.config.js new file mode 100644 index 0000000000000000000000000000000000000000..5b8652859571b0268ee85bdb0e455d1b6b6b6fa4 --- /dev/null +++ b/client/webpack.config.js @@ -0,0 +1,10 @@ +const path = require('path'); + +module.exports = { + resolve: { + alias: { + 'components': path.resolve(__dirname, 'src/components'), + }, + extensions: ['.jsx', '.js', '.scss', '.json'], + }, +}; \ No newline at end of file diff --git a/db/student.sql b/db/student.sql new file mode 100644 index 0000000000000000000000000000000000000000..06f0743ecb4a990bf69a1403afc987516b4f540c --- /dev/null +++ b/db/student.sql @@ -0,0 +1,42 @@ +-- +-- Structure de la table `STUDENT` +-- + +CREATE TABLE `STUDENT` ( + `id` int(11) NOT NULL, + `nom_utilisateur` varchar(255) NOT NULL, + `mdp` varchar(255) NOT NULL, + `nom` varchar(255) NOT NULL, + `prenom` varchar(255) NOT NULL, + `email` varchar(255) NOT NULL, + `date_naissance` date NOT NULL, + `numero_etudiant` int(20) NOT NULL, + `photo` varchar(255) NOT NULL, + `date_adhesion` date NOT NULL, + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `modified_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- +-- Déchargement des données de la table `STUDENT` +-- + +INSERT INTO `STUDENT` (`id`, `nom_utilisateur`, `mdp`, `nom`, `prenom`, `email`, `date_naissance`, `numero_etudiant`, `photo`, `date_adhesion`) VALUES +(1, 'jmercier', '36361127680f6e70448d262faf47c0459854498a74c1e2457dc7bc3f566972ce', 'Mercier', 'Jean', 'jean.mercier@etu.unistra.fr', '1999-03-02', 11223344, 'https://us.123rf.com/450wm/salamatik/salamatik1801/salamatik180100019/92979836-ic%C3%B4ne-de-visage-anonyme-de-profil-personne-silhouette-grise-avatar-par-d%C3%A9faut-masculin-photo-placeho.jpg?ver=6', '2021-09-10'), +(2, 'cbertin', 'bbb1dcf581d4af52a77eb3d2e5fee4aa40aa65ca4afa8b2ece3e511473f82e78', 'Bertin', 'Caroline', 'c.bertin@etu.unistra.fr', '1998-09-07', 99887766, 'https://us.123rf.com/450wm/salamatik/salamatik1801/salamatik180100019/92979836-ic%C3%B4ne-de-visage-anonyme-de-profil-personne-silhouette-grise-avatar-par-d%C3%A9faut-masculin-photo-placeho.jpg?ver=6', '2021-09-06'), +(3, 'mschneider', '7d68e6cb489f5347a31ecb7cce7e40de0c7f1eaba1142ea4cca35f3807ad9167', 'Schneider', 'Marine', 'm.schneider@etu.unistra.fr', '1999-02-06', 12345678, 'https://us.123rf.com/450wm/salamatik/salamatik1801/salamatik180100019/92979836-ic%C3%B4ne-de-visage-anonyme-de-profil-personne-silhouette-grise-avatar-par-d%C3%A9faut-masculin-photo-placeho.jpg?ver=6', '2021-10-01'); + +-- +-- Index pour la table `STUDENT` +-- +ALTER TABLE `STUDENT` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `nom_utilisateur` (`nom_utilisateur`), + ADD UNIQUE KEY `numero_etudiant` (`numero_etudiant`); + +-- +-- AUTO_INCREMENT pour la table `STUDENT` +-- +ALTER TABLE `STUDENT` + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4; +COMMIT; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..152e4ccc1b1282ac1e460067af37e15aa4213cb9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,55 @@ +version: "3.4" + +services: + mysql: + image: mysql:5.7 + ports: + - 3306:3306 + restart: unless-stopped + env_file: + - ./.env + volumes: + - ./db/student.sql:/docker-entrypoint-initdb.d/student.sql + + phpmyadmin: + image: phpmyadmin/phpmyadmin + depends_on: + - mysql + env_file: + - ./.env + links: + - mysql + ports: + - 8080:80 + restart: always + + server: + build: ./server + expose: + - 8000 + ports: + - 8000:8000 + volumes: + - ./server:/usr/src/app/ + depends_on: + - mysql + env_file: + - ./.env + command: > + sh -c "npm install && npm start" + + client: + build: ./client + expose: + - 3000 + ports: + - 3000:3000 + depends_on: + - server + env_file: + - ./.env + command: npm start + + +volumes: + node_modules: \ No newline at end of file diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..b512c09d476623ff4bf8d0d63c29b784925dbdf8 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..55b3addb055dc26e644707c16f892f76df6ba188 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,13 @@ +FROM node:10-alpine + +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app + +RUN npm install + +COPY package*.json /usr/src/app +RUN npm install + +COPY . /usr/src/app + +CMD ["npm", "start"] \ No newline at end of file diff --git a/server/index.js b/server/index.js index 490d97c6cb8bb923dc6053b6b054b8d28b885594..de0825df2fe7731196d02ab1f8825fa9a398e4b5 100644 --- a/server/index.js +++ b/server/index.js @@ -1,25 +1,29 @@ +const cors = require('cors'); const express = require('express'); +const mysql = require('mysql'); + const app = express(); -const port = 3001; -const unirest = require("unirest"); -app.get('/api/:word', (req, res) => { - /*const request = unirest("GET", "https://twinword-word-associations-v1.p.rapidapi.com/associations/"); - request.query({ "entry": req.params.word }); - request.headers({ - "x-rapidapi-host": "twinword-word-associations-v1.p.rapidapi.com", - "x-rapidapi-key": "YOUR_RAPID_API_KEY_GOES_HERE", - "useQueryString": true - }); +const pool = mysql.createPool({ + host: process.env.MYSQL_HOST_IP, + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, +}); - request.end(function (response) { - if (response.error) throw new Error(response.error); +app.use(cors()); - res.json(response.body.associations_scored || {}); - });*/ - res.json({ "entry": req.params.word }); +app.listen(process.env.REACT_APP_SERVER_PORT, () => { + console.log(`App server now listening on port ${process.env.REACT_APP_SERVER_PORT}`); }); -app.listen(port, () => { - console.log(`Example app listening at http://localhost:${port}`); -}); \ No newline at end of file + +app.get('/api/auth/all', (req, res) => { + pool.query(`select * from STUDENT`, (err, results) => { + if (err) { + return res.send(err); + } else { + return res.send(results); + } + }); +}); diff --git a/server/package.json b/server/package.json index e0a5da7d1980300cea7291867b1f302031f26760..086b4c48bb24c2e75e19ed65427133710aec5628 100644 --- a/server/package.json +++ b/server/package.json @@ -4,13 +4,16 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "preinstall": "npm i nodemon -g", + "start": "nodemon index.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { + "cors": "^2.8.5", "express": "^4.17.1", - "unirest": "^0.6.0" + "mysql": "^2.17.1", + "nodemon": "^2.0.12" } }