frontend et backend: Connexion d'un utilisateur

This commit is contained in:
Divlo 2020-04-06 16:46:03 +02:00
parent 8c37dbaaf4
commit 76298b6087
11 changed files with 184 additions and 31 deletions

View File

@ -35,8 +35,12 @@ exports.register = async (req, res, next) => {
exports.login = async (req, res, next) => { exports.login = async (req, res, next) => {
const { email, password } = req.body; const { email, password } = req.body;
const errors = validationResult(req);
if (!errors.isEmpty()) {
return errorHandling(next, { message: errors.array()[0].msg, statusCode: 400 });
}
try { try {
const user = await Users.findOne({ where: { email, isConfirmed: true } }); const user = await Users.findOne({ where: { email } });
if (!user) { if (!user) {
return errorHandling(next, { message: "Le mot de passe ou l'adresse email n'est pas valide.", statusCode: 400 }); return errorHandling(next, { message: "Le mot de passe ou l'adresse email n'est pas valide.", statusCode: 400 });
} }
@ -44,6 +48,9 @@ exports.login = async (req, res, next) => {
if (!isEqual) { if (!isEqual) {
return errorHandling(next, { message: "Le mot de passe ou l'adresse email n'est pas valide.", statusCode: 400 }); return errorHandling(next, { message: "Le mot de passe ou l'adresse email n'est pas valide.", statusCode: 400 });
} }
if (!user.isConfirmed) {
return errorHandling(next, { message: "Vous devez valider votre adresse email pour votre première connexion.", statusCode: 400 });
}
const token = jwt.sign({ const token = jwt.sign({
email: user.email, userId: user.id email: user.email, userId: user.id
}, JWT_SECRET, { expiresIn: '1h' }); }, JWT_SECRET, { expiresIn: '1h' });
@ -67,7 +74,7 @@ exports.confirmEmail = async (req, res, next) => {
user.tempToken = null; user.tempToken = null;
user.isConfirmed = true; user.isConfirmed = true;
await user.save(); await user.save();
return res.redirect(`${FRONT_END_HOST}/login`); return res.redirect(`${FRONT_END_HOST}/login?isConfirmed=true`);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
errorHandling(next, serverError); errorHandling(next, serverError);

View File

@ -1,12 +1,22 @@
const { Router } = require('express'); const { Router } = require('express');
const { body } = require('express-validator'); const { body } = require('express-validator');
const usersController = require('../controllers/users'); const usersController = require('../controllers/users');
const { requiredFields } = require('../assets/config/errors');
const Users = require('../models/users'); const Users = require('../models/users');
const UsersRouter = Router(); const UsersRouter = Router();
// Permet de se connecter // Permet de se connecter
UsersRouter.post('/login', usersController.login); UsersRouter.post('/login', [
body('email')
.not()
.isEmpty()
.withMessage(requiredFields.message),
body('password')
.not()
.isEmpty()
.withMessage(requiredFields.message)
], usersController.login);
// TODO: Récupère les informations public d'un profil // TODO: Récupère les informations public d'un profil
// UsersRouter.get('/profile/:userName', usersController.getUserInfo); // UsersRouter.get('/profile/:userName', usersController.getUserInfo);

View File

@ -1,10 +1,12 @@
import { useState } from 'react'; import { Fragment, useState, useContext } from 'react';
import { UserContext } from '../../contexts/UserContext';
import Link from 'next/link'; import Link from 'next/link';
import NavigationLink from './NavigationLink'; import NavigationLink from './NavigationLink';
import './Header.css'; import './Header.css';
export default function Header() { export default function Header() {
const { isAuth, logoutUser } = useContext(UserContext);
const [isActive, setIsActive] = useState(false); const [isActive, setIsActive] = useState(false);
const toggleNavbar = () => { const toggleNavbar = () => {
@ -33,8 +35,23 @@ export default function Header() {
<ul className={`navbar__list ${(isActive) ? "navbar__list-active" : ""}`}> <ul className={`navbar__list ${(isActive) ? "navbar__list-active" : ""}`}>
<NavigationLink name="Accueil" path="/" /> <NavigationLink name="Accueil" path="/" />
<NavigationLink name="Fonctions" path="/functions" /> <NavigationLink name="Fonctions" path="/functions" />
{
(!isAuth) ?
<Fragment>
<NavigationLink name="S'inscrire" path="/register" /> <NavigationLink name="S'inscrire" path="/register" />
<NavigationLink name="Connexion" path="/login" /> <NavigationLink name="Connexion" path="/login" />
</Fragment>
:
<Fragment>
<NavigationLink name="Profil" path="/profile" />
<li className="navbar-item">
<Link href={"/"}>
<a onClick={logoutUser} className="navbar-link">Se déconnecter</a>
</Link>
</li>
</Fragment>
}
</ul> </ul>
</nav> </nav>

View File

@ -0,0 +1,56 @@
import { createContext, useState, useEffect } from 'react';
import Cookies from "universal-cookie";
import api from '../utils/api';
const cookies = new Cookies();
export const UserContext = createContext();
function UserContextProvider(props) {
const [user, setUser] = useState(null);
const [isAuth, setIsAuth] = useState(false);
const [loginLoading, setLoginLoading] = useState(false);
const [messageLogin, setMessageLogin] = useState("");
useEffect(() => {
const user = cookies.get('user');
setUser(user);
if (user != undefined) {
setIsAuth(true);
}
}, []);
const loginUser = ({ email, password }) => {
setLoginLoading(true);
api.post('/users/login', { email, password })
.then((response) => {
const user = response.data;
cookies.set('user', user);
setUser(user);
setIsAuth(true);
setMessageLogin('<p class="form-success"><b>Succès:</b> Connexion réussi!</p>');
setLoginLoading(false);
})
.catch((error) => {
setMessageLogin(`<p class="form-error"><b>Erreur:</b> ${error.response.data.message}</p>`);
setLoginLoading(false);
setIsAuth(false);
setUser(null);
});
}
const logoutUser = () => {
setUser(null);
setIsAuth(false);
cookies.remove('user');
}
return (
<UserContext.Provider value={{ user, loginUser, logoutUser, loginLoading, messageLogin, isAuth }}>
{props.children}
</UserContext.Provider>
);
}
export default UserContextProvider;

View File

@ -1400,11 +1400,21 @@
"resolved": "https://registry.npmjs.org/@next/polyfill-nomodule/-/polyfill-nomodule-9.3.2.tgz", "resolved": "https://registry.npmjs.org/@next/polyfill-nomodule/-/polyfill-nomodule-9.3.2.tgz",
"integrity": "sha512-kEa7v3trZmW6iWeTJrhg+ZsE9njae7mLkgyZB5M1r975JHr5PQ69B5aX7hrEAj7aAJYvCKETgAczx4gGR8MOzQ==" "integrity": "sha512-kEa7v3trZmW6iWeTJrhg+ZsE9njae7mLkgyZB5M1r975JHr5PQ69B5aX7hrEAj7aAJYvCKETgAczx4gGR8MOzQ=="
}, },
"@types/cookie": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
},
"@types/domhandler": { "@types/domhandler": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/@types/domhandler/-/domhandler-2.4.1.tgz", "resolved": "https://registry.npmjs.org/@types/domhandler/-/domhandler-2.4.1.tgz",
"integrity": "sha512-cfBw6q6tT5sa1gSPFSRKzF/xxYrrmeiut7E0TxNBObiLSBTuFEHibcfEe3waQPEDbqBsq+ql/TOniw65EyDFMA==" "integrity": "sha512-cfBw6q6tT5sa1gSPFSRKzF/xxYrrmeiut7E0TxNBObiLSBTuFEHibcfEe3waQPEDbqBsq+ql/TOniw65EyDFMA=="
}, },
"@types/object-assign": {
"version": "4.0.30",
"resolved": "https://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz",
"integrity": "sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI="
},
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.8.5", "version": "1.8.5",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
@ -9774,6 +9784,17 @@
"imurmurhash": "^0.1.4" "imurmurhash": "^0.1.4"
} }
}, },
"universal-cookie": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.3.tgz",
"integrity": "sha512-YbEHRs7bYOBTIWedTR9koVEe2mXrq+xdjTJZcoKJK/pQaE6ni28ak2AKXFpevb+X6w3iU5SXzWDiJkmpDRb9qw==",
"requires": {
"@types/cookie": "^0.3.3",
"@types/object-assign": "^4.0.30",
"cookie": "^0.4.0",
"object-assign": "^4.1.1"
}
},
"unpipe": { "unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",

View File

@ -17,7 +17,8 @@
"react": "16.13.0", "react": "16.13.0",
"react-dom": "16.13.0", "react-dom": "16.13.0",
"react-swipeable-views": "^0.13.9", "react-swipeable-views": "^0.13.9",
"react-swipeable-views-utils": "^0.13.9" "react-swipeable-views-utils": "^0.13.9",
"universal-cookie": "^4.0.3"
}, },
"devDependencies": { "devDependencies": {
"cross-env": "^7.0.2" "cross-env": "^7.0.2"

View File

@ -16,13 +16,6 @@ const Error404 = () => (
Cette page n'existe pas! <Link href={"/"}><a>Revenir à la page d'accueil ?</a></Link> Cette page n'existe pas! <Link href={"/"}><a>Revenir à la page d'accueil ?</a></Link>
</p> </p>
</div> </div>
<style>
{`
#__next {
padding-top: 0;
}
`}
</style>
</Fragment> </Fragment>
); );

View File

@ -1,5 +1,4 @@
/* Libraries Imports */ /* Libraries Imports */
import { Fragment } from 'react';
import Router from 'next/router' import Router from 'next/router'
import NProgress from 'nprogress'; import NProgress from 'nprogress';
@ -7,6 +6,9 @@ import NProgress from 'nprogress';
import Header from '../components/Header/Header'; import Header from '../components/Header/Header';
import Footer from '../components/Footer/Footer'; import Footer from '../components/Footer/Footer';
/* Contexts Imports */
import UserContextProvider from '../contexts/UserContext';
/* CSS Imports */ /* CSS Imports */
import '../public/fonts/Montserrat/Montserrat.css'; import '../public/fonts/Montserrat/Montserrat.css';
import '../public/css/normalize.css'; import '../public/css/normalize.css';
@ -19,13 +21,13 @@ Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done()); Router.events.on('routeChangeError', () => NProgress.done());
const App = ({ Component, pageProps }) => ( const App = ({ Component, pageProps }) => (
<Fragment> <UserContextProvider>
<Header /> <Header />
<div className="content container-fluid"> <div className="content container-fluid">
<Component {...pageProps} /> <Component {...pageProps} />
</div> </div>
<Footer /> <Footer />
</Fragment> </UserContextProvider>
); );
export default App; export default App;

View File

@ -1,9 +1,29 @@
import { Fragment } from 'react'; import { Fragment, useContext, useState } from 'react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import htmlParser from 'html-react-parser';
import Loader from '../components/Loader';
import HeadTag from '../components/HeadTag'; import HeadTag from '../components/HeadTag';
import { UserContext } from '../contexts/UserContext';
import '../public/css/pages/register-login.css'; import '../public/css/pages/register-login.css';
const Login = () => { const Login = () => {
const router = useRouter();
const [inputState, setInputState] = useState({});
const { loginUser, messageLogin, loginLoading } = useContext(UserContext);
const handleChange = () => {
const inputStateNew = { ...inputState };
inputStateNew[event.target.name] = event.target.value;
setInputState(inputStateNew);
}
const handleSubmit = (event) => {
event.preventDefault();
loginUser(inputState);
}
return ( return (
<Fragment> <Fragment>
<HeadTag <HeadTag
@ -14,26 +34,40 @@ const Login = () => {
<div className="row Register-Login__row justify-content-center"> <div className="row Register-Login__row justify-content-center">
<div className="col-20"> <div className="col-20">
<h1 className="Register-Login__title">Se connecter</h1> <h1 className="Register-Login__title">Se connecter</h1>
<form> <form onSubmit={handleSubmit}>
<div className="form-group"> <div className="form-group">
<label className="form-label" htmlFor="name">Email :</label> <label className="form-label" htmlFor="email">Email :</label>
<input type="email" name="email" id="email" className="form-control" placeholder="email@gmail.com" /> <input onChange={handleChange} type="email" name="email" id="email" className="form-control" placeholder="email@gmail.com" />
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="form-label" htmlFor="name">Mot de passe :</label> <label className="form-label" htmlFor="password">Mot de passe :</label>
<input type="password" name="password" id="password" className="form-control" placeholder="******" /> <input onChange={handleChange} type="password" name="password" id="password" className="form-control" placeholder="******" />
<p>
<Link href={"/register"}>
<a className="Register-Login__Forgot-password">Mot de passe oublié ?</a>
</Link>
</p>
</div> </div>
<div className="form-group text-center"> <div className="form-group text-center">
<button type="submit" className="btn btn-dark">Envoyer</button> <button type="submit" className="btn btn-dark">Envoyer</button>
</div> </div>
</form> </form>
<div className="form-result text-center"></div> <div className="form-result text-center">
{(router.query.isConfirmed !== undefined) && <p className="form-success"><b>Succès:</b> Votre compte a bien été confirmé, vous pouvez maintenant vous connectez!</p>}
{
(loginLoading) ?
<Loader />
:
htmlParser(messageLogin)
}
</div>
</div> </div>
</div> </div>
</div> </div>
</Fragment> </Fragment>
); );
} }
export default Login; export default Login;

View File

@ -12,3 +12,11 @@
.Register-Login__title { .Register-Login__title {
text-align: center; text-align: center;
} }
.Register-Login__Forgot-password {
color: var(--text-color);
font-size: 16px;
}
.Register-Login__Forgot-password:hover {
color: var(--important);
text-decoration: none;
}

View File

@ -1,6 +1,10 @@
function redirect(ctx, path) { function redirect(ctx, path) {
if (ctx.res) {
ctx.res.writeHead(302, { Location: path }); ctx.res.writeHead(302, { Location: path });
ctx.res.end(); ctx.res.end();
} else {
document.location.pathname = path;
}
} }
module.exports = redirect; module.exports = redirect;