diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..aea7cd7 --- /dev/null +++ b/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "esmodules": true + } + } + ] + ] +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a882442 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 diff --git a/.eslintrc.js b/.eslintrc.js index b864f33..5a6a105 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,22 +1,30 @@ module.exports = { - 'env': { - 'es6': true, - 'node': true, - }, - 'extends': [ - 'google', - ], - 'globals': { - 'Atomics': 'readonly', - 'SharedArrayBuffer': 'readonly', - }, - 'parserOptions': { - 'ecmaVersion': 2018, - 'sourceType': 'module', - }, - 'rules': { - 'max-len': ['error', {'code': 180}], - 'new-cap': ['error', {'capIsNewExceptions': ['ENUM', 'ARRAY', 'TEXT', 'STRING', 'ObjectId']}], - }, + env: { + browser: true, + es2020: true, + node: true, + jquery: true, + }, + extends: ['airbnb-base', 'prettier'], + plugins: ['prettier'], + parserOptions: { + ecmaVersion: 11, + sourceType: 'module', + }, + rules: { + 'prettier/prettier': ['error'], + 'no-underscore-dangle': [ + 'error', + { + allow: ['_id'], + }, + ], + }, + ignorePatterns: ['public/libs/**/*.js', 'public/js/main.js', 'dist/**'], + overrides: [ + { + files: ['**/*.js'], + excludedFiles: '*.ejs', + }, + ], }; - diff --git a/package.json b/package.json index 06908e5..d049d8e 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,16 @@ "scripts": { "start": "node ./dist/bin/www", "dev": "npm-run-all build start", - "watch": "nodemon", + "watch": "nodemon -e js,ejs", "prebuild": "rimraf dist", - "build": "babel ./src --out-dir dist", + "build": "babel ./src --out-dir dist --copy-files", "test": "jest", "prepare": "husky install" }, + "engines": { + "node": "16.x", + "yarn": "1.x" + }, "repository": { "type": "git", "url": "git@git.darkou.fr:dbroqua/nodecdtheque.git" @@ -26,15 +30,31 @@ "@babel/core": "^7.17.2", "@babel/preset-env": "^7.16.11", "eslint": "^8.9.0", - "eslint-config-google": "^0.14.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.25.4", + "eslint-plugin-prettier": "^4.0.0", "husky": "^7.0.4", "lint-staged": "^12.3.3", "nodemon": "^2.0.15", "npm-run-all": "^4.1.5", + "prettier": "^2.5.1", "rimraf": "^3.0.2" }, "dependencies": { - "express": "^4.17.2" + "connect-ensure-login": "^0.1.1", + "connect-flash": "^0.1.1", + "connect-mongo": "^4.6.0", + "cookie-parser": "^1.4.6", + "ejs": "^3.1.6", + "express": "^4.17.2", + "express-session": "^1.17.2", + "jquery": "^3.6.0", + "mdbootstrap": "^4.20.0", + "mongoose": "^6.2.1", + "mongoose-unique-validator": "^3.0.0", + "passport": "^0.5.2", + "passport-local": "^1.0.0" }, "nodemonConfig": { "exec": "npm run dev", @@ -47,11 +67,6 @@ "*.spec.js" ] }, - "babel": { - "presets": [ - "@babel/preset-env" - ] - }, "jest": { "testEnvironment": "node" }, diff --git a/public/css/main.css b/public/css/main.css new file mode 100644 index 0000000..e69de29 diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..a06747b Binary files /dev/null and b/public/favicon.png differ diff --git a/public/img/404.svg b/public/img/404.svg new file mode 100644 index 0000000..c963bb6 --- /dev/null +++ b/public/img/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/logo.png b/public/img/logo.png new file mode 100644 index 0000000..bbb404c Binary files /dev/null and b/public/img/logo.png differ diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..e69de29 diff --git a/src/app.js b/src/app.js index 9546de8..0ca0bf5 100644 --- a/src/app.js +++ b/src/app.js @@ -1,14 +1,119 @@ -import express from 'express'; -import path from 'path'; +import path from "path"; +import express from "express"; +import cookieParser from "cookie-parser"; +import passport from "passport"; +import mongoose from "mongoose"; +import flash from "connect-flash"; +import session from "express-session"; +import MongoStore from "connect-mongo"; -import indexRouter from './routes/index'; +import config, { env, mongoDbUri, secret } from "./config"; + +import indexRouter from "./routes/index"; + +// Mongoose schema init +require("./models/users"); + +require("./libs/passport")(passport); + +mongoose + .connect(mongoDbUri, { useNewUrlParser: true, useUnifiedTopology: true }) + .catch(() => { + process.exit(); + }); + +const sess = { + cookie: { + maxAge: 86400000, + }, + secret, + saveUninitialized: false, + resave: false, + store: MongoStore.create({ + mongoUrl: mongoDbUri, + mongoOptions: { useNewUrlParser: true, useUnifiedTopology: true }, + }), +}; const app = express(); app.use(express.json()); -app.use(express.urlencoded({extended: false})); -app.use(express.static(path.join(__dirname, '../public'))); +app.use(express.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(flash()); -app.use('/', indexRouter); +app.use(session(sess)); + +if (["production"].indexOf(env) !== -1) { + app.enable("trust proxy", 1); + sess.cookie.secure = true; + /* eslint-disable func-names */ + app.use((req, res, next) => { + if (req.secure) { + // request was via https, so do no special handling + next(); + } else { + // request was via http, so redirect to https + res.redirect(`https://${req.headers.host}${req.url}`); + } + }); +} + +app.use(passport.initialize()); +app.use(passport.session()); + +app.set("views", path.join(__dirname, "views")); +app.set("view engine", "ejs"); + +app.use(express.static(path.join(__dirname, "../public"))); +app.use( + "/libs/jquery", + express.static(path.join(__dirname, "../node_modules/jquery/dist/")) +); +app.use( + "/libs/mdbootstrap", + express.static(path.join(__dirname, "../node_modules/mdbootstrap")) +); + +app.use("/", indexRouter); + +// Handle 404 +app.use((req, res) => { + if (req.xhr) { + res.status(404).send({ message: "404: Not found" }); + } else { + res.status(404).render("error", { + page: { title: `404: Cette page n'existe pas.` }, + errorCode: 404, + user: req.user || null, + config, + session: req.session || null, + flashInfo: null, + query: null, + params: null, + }); + } +}); + +// Handle 500 +app.use((error, req, res, next) => { + if (req.xhr) { + res.status(error.errorCode || 500).send({ message: error.message }); + } else { + res.status(500); + res.render("error", { + page: { title: "500: Oups… le serveur a crashé !", error }, + errorCode: error.errorCode || 500, + user: req.user || null, + config, + session: req.session || null, + flashInfo: null, + query: null, + params: null, + }); + + next(); + } +}); export default app; diff --git a/src/config/index.js b/src/config/index.js index 92d8438..4577bd4 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -1,4 +1,6 @@ module.exports = { - nodeEnv: process.env.NODE_ENV || 'development', - port: parseInt(process.env.PORT || '3001', 10), + nodeEnv: process.env.NODE_ENV || "development", + port: parseInt(process.env.PORT || "3001", 10), + mongoDbUri: process.env.MONGODB_URI || "mongodb://nodecdtheque-db/cdtheque", + secret: process.env.SECRET || "waemaeMe5ahc6ce1chaeKohKa6Io8Eik", }; diff --git a/src/helpers/index.js b/src/helpers/index.js new file mode 100644 index 0000000..c03e285 --- /dev/null +++ b/src/helpers/index.js @@ -0,0 +1,3 @@ +/* eslint-disable import/prefer-default-export */ + +export const getBaseUrl = (req) => `${req.protocol}://${req.get("host")}`; diff --git a/src/libs/format.js b/src/libs/format.js new file mode 100644 index 0000000..abe1b7b --- /dev/null +++ b/src/libs/format.js @@ -0,0 +1,3 @@ +export default (res, page) => { + res.status(200).render("index", page.render()); +}; diff --git a/src/libs/passport.js b/src/libs/passport.js new file mode 100644 index 0000000..1b46ab0 --- /dev/null +++ b/src/libs/passport.js @@ -0,0 +1,39 @@ +/* eslint-disable func-names */ +const mongoose = require("mongoose"); +const LocalStrategy = require("passport-local").Strategy; + +const Users = mongoose.model("Users"); + +module.exports = function (passport) { + passport.serializeUser((user, done) => { + done(null, user); + }); + + passport.deserializeUser((user, done) => { + done(null, user); + }); + passport.use( + "user", + new LocalStrategy( + { + usernameField: "email", + passwordField: "password", + }, + (email, password, done) => { + Users.findOne({ email }) + .then((user) => { + if (!user || !user.validPassword(password)) { + return done( + null, + false, + "Oops! Identifiants incorrects" + ); + } + + return done(null, user); + }) + .catch(done); + } + ) + ); +}; diff --git a/src/middleware/Auth.js b/src/middleware/Auth.js new file mode 100644 index 0000000..c20df57 --- /dev/null +++ b/src/middleware/Auth.js @@ -0,0 +1,44 @@ +import Pages from "./Pages"; + +import Users from "../models/users"; + +/** + * Classe permettant la gestion des utilisateurs + */ +class Auth extends Pages { + /** + * Méthode permettant de créer un nouvel utilisateur + * @param {Req} req + * + * @return {Object} + */ + static async register(req) { + try { + const { username, email, password } = req.body; + const user = new Users({ + username, + email, + salt: password, + }); + + const resUser = await user.save(); + + await new Promise((resolve, reject) => { + req.login(resUser, (errLogin) => { + if (errLogin) { + return reject(errLogin); + } + + return resolve(null); + }); + }); + + return resUser; + } catch (err) { + req.flash("error", err.toString()); + throw err; + } + } +} + +export default Auth; diff --git a/src/middleware/Pages.js b/src/middleware/Pages.js new file mode 100644 index 0000000..198c063 --- /dev/null +++ b/src/middleware/Pages.js @@ -0,0 +1,62 @@ +import config from "../config"; +import { getBaseUrl } from "../helpers"; + +/** + * Classe permettant de gérer les page du back office + */ +class Pages { + /** + * @param {Object} req + * @param {String} viewname + */ + constructor(req, viewname) { + this.req = req; + this.pageContent = { + page: { + title: null, + user: null, + }, + viewname: `pages/${viewname}`, + }; + + this.user = null; + this.pagename = viewname; + + if (req.session && req.session.passport && req.session.passport.user) { + this.user = req.session.passport.user; + } + + if (!req.query.page) { + req.query.page = 1; + } + if (!req.query.limit) { + req.query.limit = config.pagination; + } + } + + /** + * Rendu de la page + * @return {Object} + */ + render() { + this.pageContent.session = this.req.session; + this.pageContent.flashInfo = this.req.flash("info"); + this.pageContent.error = this.req.flash("error") || null; + this.pageContent.query = this.req.query; + this.pageContent.params = this.req.params; + this.pageContent.user = this.user; + this.pageContent.config = config; + this.pageContent.getBaseUrl = getBaseUrl(); + + if (this.req.session.flash && this.req.session.flash.error) { + // eslint-disable-next-line prefer-destructuring + this.pageContent.page.failureFlash = + this.req.session.flash.error[0]; + this.req.session.flash = null; + } + + return this.pageContent; + } +} + +export default Pages; diff --git a/src/models/users.js b/src/models/users.js new file mode 100644 index 0000000..0807ccb --- /dev/null +++ b/src/models/users.js @@ -0,0 +1,58 @@ +/* eslint-disable func-names */ +/* eslint-disable no-invalid-this */ +import mongoose from "mongoose"; +import uniqueValidator from "mongoose-unique-validator"; +import crypto from "crypto"; + +const UserSchema = new mongoose.Schema( + { + username: { + type: String, + unique: true, + required: [true, "est requis"], + match: [/^[a-zA-Z0-9]+$/, "est invalide"], + index: true, + }, + email: { + type: String, + lowercase: true, + unique: true, + required: [true, "est requis"], + match: [/\S+@\S+\.\S+/, "est invalide"], + index: true, + }, + hash: String, + salt: String, + }, + { timestamps: true } +); + +UserSchema.plugin(uniqueValidator, { message: "est déjà utilisé" }); + +UserSchema.methods.validPassword = function (password) { + const [salt, key] = this.hash.split(":"); + + return key === crypto.scryptSync(password, salt, 64).toString("hex"); +}; + +UserSchema.pre("save", function (next) { + const user = this; + + if (!user.isModified("salt")) { + return next(); + } + + const salt = crypto.randomBytes(16).toString("hex"); + + return crypto.scrypt(user.salt, salt, 64, (err, derivedKey) => { + if (err) { + next(err); + } + this.salt = salt; + this.hash = `${salt}:${derivedKey.toString("hex")}`; + + next(); + }); +}); + +export default mongoose.model("Users", UserSchema); diff --git a/src/routes/index.js b/src/routes/index.js index 472ba28..e2cb331 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,11 +1,82 @@ -import express from 'express'; +import express from "express"; +import passport from "passport"; +import { ensureLoggedIn } from "connect-ensure-login"; + +import Pages from "../middleware/Pages"; +import Auth from "../middleware/Auth"; + +import render from "../libs/format"; // eslint-disable-next-line new-cap const router = express.Router(); -/* GET home page. */ -router.get('/', function(req, res, next) { - res.render('index', {title: 'World'}); +router + .route("/") + .get(ensureLoggedIn("/connexion"), (req, res) => + res.redirect("/ma-collection") + ); + +router + .route("/connexion") + .get((req, res, next) => { + try { + const page = new Pages(req, "connexion"); + + render(res, page); + } catch (err) { + next(err); + } + }) + .post( + passport.authenticate("user", { + failureRedirect: "/connexion", + failureFlash: true, + }), + (req, res) => { + const { next, query } = req.body; + let url = `/${next}`; + + if (next) { + if (query) { + const params = JSON.parse(query); + Object.keys(params).forEach((key) => { + url += `${url.indexOf("?") === -1 ? "?" : "&"}${key}=${ + params[key] + }`; + }); + } + return res.redirect(url); + } + return res.redirect("/"); + } + ); + +router + .route("/inscription") + .get((req, res, next) => { + try { + const page = new Pages(req, "inscription"); + + render(res, page); + } catch (err) { + next(err); + } + }) + .post(async (req, res) => { + try { + await Auth.register(req); + + res.redirect("/"); + } catch (err) { + res.redirect("/inscription"); + } + }); + +router.route("/se-deconnecter").get((req, res) => { + req.logout(); + req.session.destroy(() => { + res.redirect("/"); + }); }); export default router; diff --git a/src/views/error.ejs b/src/views/error.ejs new file mode 100644 index 0000000..d2057dd --- /dev/null +++ b/src/views/error.ejs @@ -0,0 +1,23 @@ + + +<%- include('partials/head', {page: page, user: user}); %> + + + <%- include('partials/header'); %> +
+
+
+

<%= page.title %>

+ <% if ( errorCode && errorCode === 404 ) { %> + Erreur 404 + <% } %> +

+ <%= page.error %> +

+
+
+
+ <%- include('partials/footer', {page: page, user: user, blog: null}); %> + + + \ No newline at end of file diff --git a/src/views/index.ejs b/src/views/index.ejs new file mode 100644 index 0000000..4c21a1f --- /dev/null +++ b/src/views/index.ejs @@ -0,0 +1,26 @@ + + + <%- include('partials/head'); %> + + <%- include('partials/header'); %> + + <% if ( page.failureFlash ) {%> + + <% } %> + <% + if (error && error.length > 0) { + for( let i = 0 ; i < error.length ; i += 1 ) { + %> + + <% + } + } + %> + <%- include(viewname) %> + <%- include('partials/footer'); %> + + diff --git a/src/views/pages/connexion.ejs b/src/views/pages/connexion.ejs new file mode 100644 index 0000000..5c3c444 --- /dev/null +++ b/src/views/pages/connexion.ejs @@ -0,0 +1,26 @@ +
+
+
+
+ DarKou +

Connexion

+ +
+ + +
+ +
+ + +
+ + + +

Pas encore inscrit ? + Inscrivez-vous +

+
+
+
+
\ No newline at end of file diff --git a/src/views/pages/inscription.ejs b/src/views/pages/inscription.ejs new file mode 100644 index 0000000..9ca3a04 --- /dev/null +++ b/src/views/pages/inscription.ejs @@ -0,0 +1,31 @@ +
+
+
+
+ DarKou +

Inscription

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +

Déjà inscrit ? + Connectez-vous +

+
+
+
+
\ No newline at end of file diff --git a/src/views/partials/footer.ejs b/src/views/partials/footer.ejs new file mode 100644 index 0000000..b929419 --- /dev/null +++ b/src/views/partials/footer.ejs @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/views/partials/head.ejs b/src/views/partials/head.ejs new file mode 100644 index 0000000..7b0f74f --- /dev/null +++ b/src/views/partials/head.ejs @@ -0,0 +1,18 @@ + + + + + + <% if (page.title) { %><%= page.title %> <% } else { %> DarKou - Ma CDThèque <% } %> + + + + + + + + + + + + \ No newline at end of file diff --git a/src/views/partials/header.ejs b/src/views/partials/header.ejs new file mode 100644 index 0000000..74d6c0e --- /dev/null +++ b/src/views/partials/header.ejs @@ -0,0 +1,25 @@ + \ No newline at end of file