diff --git a/.eslintrc.js b/.eslintrc.js index 5a6a105..c8f8a9d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,7 +16,13 @@ module.exports = { 'no-underscore-dangle': [ 'error', { - allow: ['_id'], + allow: ['_id', 'artists_sort'], + }, + ], + 'camelcase': [ + 'error', + { + allow: ['artists_sort',] }, ], }, diff --git a/.gitignore b/.gitignore index 17e19c3..558a321 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,4 @@ dist dist yarn.lock +public/css diff --git a/README.md b/README.md index 53f47c8..c8dc26a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,33 @@ # nodecdtheque +NodeCDThèque (non temporaire faute de mieux haha) est une application Web permettant de lister votre collection de CD ou vinyles. + +## Prérequis + +Pour fonctionner vous devez avoir `docker` d'installer sur votre serveur. + +## Installation + +### Préparer la terre + +Après avoir cloné le projet il faudra créer un fichier d'environnement nommé `.env` et situé à la racine de projet. + +Contenu du fichier : +``` +NODE_ENV=development +DISCOGS_TOKEN=*** +``` + +Pour obtenir un token Discogs vous devez simplement vous inscrire sur [Discogs](https://www.discogs.com). Une fois connecté à votre espace Discogs vous pourrez obtenir un token en allant dans le sous-menu [Développeur](https://www.discogs.com/settings/developers). + +### Semer + +Vous pouvez maintenant simplement lancer la commande suivante afin de démarrer le projet en tant que service : + +``` +docker-compose up -d +``` + +### Pailler + +Une fois le serveur démarré vous pourrez accéder à l'interface web via [http://127.0.0.1:3001](http://127.0.0.1:3001). (ou tout autre port si vous avez défini `PORT` dans le fichier `.env`). diff --git a/docker-compose.yml b/docker-compose.yml index 0c4ca4c..ffc15e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,8 @@ services: depends_on: - nodecdtheque-db environment: - NODE_ENV: "development" + NODE_ENV: ${NODE_ENV} + DISCOGS_TOKEN: ${DISCOGS_TOKEN} networks: - nodecdtheque nodecdtheque-db: diff --git a/package.json b/package.json index 6d70680..4d63018 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "description": "Simple application to manage your CD/Vinyl collection", "scripts": { "start": "node ./dist/bin/www", - "dev": "npm-run-all build start", - "watch": "nodemon -e js,ejs", + "dev": "npm-run-all build sass start", + "watch": "nodemon", + "sass": "npx sass sass/*.scss public/css/main.css -s compressed", "prebuild": "rimraf dist", "build": "babel ./src --out-dir dist --copy-files", "test": "jest", @@ -42,20 +43,26 @@ "rimraf": "^3.0.2" }, "dependencies": { + "axios": "^0.26.0", "connect-ensure-login": "^0.1.1", "connect-flash": "^0.1.1", "connect-mongo": "^4.6.0", "cookie-parser": "^1.4.6", "debug": "^4.3.3", + "disconnect": "^1.2.2", "ejs": "^3.1.6", "express": "^4.17.2", "express-session": "^1.17.2", "jquery": "^3.6.0", - "mdbootstrap": "^4.20.0", + "mdb-ui-kit": "^3.10.2", + "moment": "^2.29.1", "mongoose": "^6.2.1", "mongoose-unique-validator": "^3.0.0", "passport": "^0.5.2", - "passport-local": "^1.0.0" + "passport-http": "^0.3.0", + "passport-local": "^1.0.0", + "sass": "^1.49.7", + "vue": "^3.2.31" }, "nodemonConfig": { "exec": "npm run dev", diff --git a/public/css/main.css b/public/css/main.css index e69de29..5894bdb 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -0,0 +1 @@ +#toastr{visibility:hidden;min-width:250px;margin-left:-125px;background-color:#9c3030;color:#fff;text-align:left;border-radius:2px;padding:16px;position:fixed;z-index:1;right:30px;top:30px;font-size:17px}#toastr.show{visibility:visible;animation:toastrFadein .5s,toastrFadeout .5s 2.5s}@keyframes toastrFadein{from{top:0;opacity:0}to{top:30px;opacity:1}}@keyframes toastrFadeout{from{top:30px;opacity:1}to{top:0;opacity:0}}/*# sourceMappingURL=main.css.map */ diff --git a/public/js/main.js b/public/js/main.js deleted file mode 100644 index e69de29..0000000 diff --git a/sass/toast.scss b/sass/toast.scss new file mode 100644 index 0000000..3cb8e01 --- /dev/null +++ b/sass/toast.scss @@ -0,0 +1,42 @@ +#toastr { + visibility: hidden; + min-width: 250px; + margin-left: -125px; + background-color: rgb(156, 48, 48); + color: #fff; + text-align: left; + border-radius: 2px; + padding: 16px; + position: fixed; + z-index: 1; + right: 30px; + top: 30px; + font-size: 17px; + + &.show { + visibility: visible; + animation: toastrFadein 0.5s, toastrFadeout 0.5s 2.5s; + } +} + +@keyframes toastrFadein { + from { + top: 0; + opacity: 0; + } + to { + top: 30px; + opacity: 1; + } +} + +@keyframes toastrFadeout { + from { + top: 30px; + opacity: 1; + } + to { + top: 0; + opacity: 0; + } +} \ No newline at end of file diff --git a/src/app.js b/src/app.js index 50c4eeb..f5a00fe 100644 --- a/src/app.js +++ b/src/app.js @@ -9,10 +9,14 @@ import MongoStore from "connect-mongo"; import config, { env, mongoDbUri, secret } from "./config"; -import indexRouter from "./routes/index"; +import indexRouter from "./routes"; +import addAlbumRouter from "./routes/addAlbum"; +import importRouterApiV1 from "./routes/api/v1"; +import importAlbumRouterApiV1 from "./routes/api/v1/albums"; // Mongoose schema init require("./models/users"); +require("./models/albums"); require("./libs/passport")(passport); @@ -60,7 +64,7 @@ if (["production"].indexOf(env) !== -1) { app.use(passport.initialize()); app.use(passport.session()); -app.set("views", path.join(__dirname, "views")); +app.set("views", path.join(__dirname, "../views")); app.set("view engine", "ejs"); app.use(express.static(path.join(__dirname, "../public"))); @@ -69,45 +73,65 @@ app.use( express.static(path.join(__dirname, "../node_modules/jquery/dist/")) ); app.use( - "/libs/mdbootstrap", - express.static(path.join(__dirname, "../node_modules/mdbootstrap")) + "/libs/mdb-ui-kit/css", + express.static(path.join(__dirname, "../node_modules/mdb-ui-kit/css")) +); +app.use( + "/libs/mdb-ui-kit/js", + express.static(path.join(__dirname, "../node_modules/mdb-ui-kit/js")) +); +app.use( + "/libs/vue", + express.static(path.join(__dirname, "../node_modules/vue/dist")) +); +app.use( + "/libs/axios", + express.static(path.join(__dirname, "../node_modules/axios/dist")) ); app.use("/", indexRouter); +app.use("/ajouter-un-album", addAlbumRouter); +app.use("/api/v1", importRouterApiV1); +app.use("/api/v1/albums", importAlbumRouterApiV1); // Handle 404 app.use((req, res) => { - if (req.xhr) { + if (req.xhr || req.rawHeaders.indexOf("application/json") !== -1) { res.status(404).send({ message: "404: Not found" }); } else { - res.status(404).render("error", { + res.status(404).render("index", { page: { title: `404: Cette page n'existe pas.` }, errorCode: 404, + viewname: "error", user: req.user || null, config, session: req.session || null, flashInfo: null, query: null, params: null, + error: null, }); } }); // Handle 500 app.use((error, req, res, next) => { - if (req.xhr) { - res.status(error.errorCode || 500).send({ message: error.message }); + if (req.xhr || req.rawHeaders.indexOf("application/json") !== -1) { + const { message, errorCode, date } = error; + res.status(error.errorCode || 500).send({ message, errorCode, date }); } else { - res.status(500); - res.render("error", { + res.status(error.errorCode || 500); + res.render("index", { page: { title: "500: Oups… le serveur a crashé !", error }, errorCode: error.errorCode || 500, + viewname: "error", user: req.user || null, config, session: req.session || null, flashInfo: null, query: null, params: null, + error: null, }); next(); diff --git a/src/config/index.js b/src/config/index.js index 4577bd4..e8f4a6e 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -3,4 +3,5 @@ module.exports = { port: parseInt(process.env.PORT || "3001", 10), mongoDbUri: process.env.MONGODB_URI || "mongodb://nodecdtheque-db/cdtheque", secret: process.env.SECRET || "waemaeMe5ahc6ce1chaeKohKa6Io8Eik", + discogsToken: process.env.DISCOGS_TOKEN, }; diff --git a/src/helpers/index.js b/src/helpers/index.js index c03e285..95f2ba1 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -1,3 +1,25 @@ /* eslint-disable import/prefer-default-export */ +import { Client as Discogs } from "disconnect"; + +import { discogsToken } from "../config"; export const getBaseUrl = (req) => `${req.protocol}://${req.get("host")}`; + +export const searchSong = async (q) => { + const dis = new Discogs({ userToken: discogsToken }).database(); + + const res = await dis.search({ + q, + type: "release", + }); + + return res; +}; + +export const getAlbumDetails = async (id) => { + const dis = new Discogs({ userToken: discogsToken }).database(); + + const res = await dis.getRelease(id); + + return res; +}; diff --git a/src/libs/error.js b/src/libs/error.js new file mode 100644 index 0000000..562265f --- /dev/null +++ b/src/libs/error.js @@ -0,0 +1,21 @@ +/** + * Classe permettant la gestion des erreurs personilisées + */ +class ErrorEvent extends Error { + /** + * @param {Number} errorCode + * @param {Mixed} ...params + */ + constructor(errorCode, ...params) { + super(...params); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ErrorEvent); + } + + this.errorCode = parseInt(errorCode, 10); + this.date = new Date(); + } +} + +export default ErrorEvent; diff --git a/src/libs/format.js b/src/libs/format.js index abe1b7b..14dfaf9 100644 --- a/src/libs/format.js +++ b/src/libs/format.js @@ -1,3 +1,39 @@ +/** + * Fonction permettant de formater une réponse basée sur la méthode utilisée + * @param {Object} req + * @param {Object} res + * @param {Object} data + * + * @return {Object} + */ +export const sendResponse = (req, res, data) => { + let status = 200; + const path = req.route.path.split("/"); + + switch (req.method) { + case "GET": + // INFO: On regarde de quel type de get il s'agit (liste ou item) + if (path[path.length - 1].indexOf(":") === 0) { + // INFO: Cas d'un item + status = !data ? 404 : 200; + } else { + // INFO: Cas d'une liste + status = + data.rows === undefined || data.rows.length > 0 ? 200 : 204; + } + + return res.status(status).json(data).end(); + case "PATCH": + return res.status(200).json(data).end(); + case "DELETE": + return res.status(200).json(data).end(); + case "POST": + return res.status(201).json(data).end(); + default: + return res.status(500).json({ message: "Not implemented" }); + } +}; + export default (res, page) => { res.status(200).render("index", page.render()); }; diff --git a/src/libs/passport.js b/src/libs/passport.js index 1b46ab0..af0ab0b 100644 --- a/src/libs/passport.js +++ b/src/libs/passport.js @@ -1,6 +1,7 @@ /* eslint-disable func-names */ const mongoose = require("mongoose"); const LocalStrategy = require("passport-local").Strategy; +const { BasicStrategy } = require("passport-http"); const Users = mongoose.model("Users"); @@ -36,4 +37,22 @@ module.exports = function (passport) { } ) ); + passport.use( + "basic", + new BasicStrategy((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/Albums.js b/src/middleware/Albums.js new file mode 100644 index 0000000..821267d --- /dev/null +++ b/src/middleware/Albums.js @@ -0,0 +1,85 @@ +import moment from "moment"; + +import Pages from "./Pages"; + +import { getAlbumDetails } from "../helpers"; + +import AlbumsModel from "../models/albums"; + +/** + * Classe permettant la gestion des albums d'un utilisateur + */ +class Albums extends Pages { + async getFormAddOne() { + const data = await getAlbumDetails(this.req.params.discogsId); + + const { + id, // Integer + year, // - Integer + uri, // String + artists, // - Array + artists_sort, // String + labels, // - Array + series, // Array + companies, // - Array + formats, // - Array + title, // - String + country, // - String + released, // - Date + notes, // - String + identifiers, // - Array + videos, // - Array + genres, // - Array + styles, // - Array + tracklist, // - Array + extraartists, // - Array + images, // - Array { + try { + const page = new Pages(req, "ajouter-un-album/search"); + + render(res, page); + } catch (err) { + next(err); + } +}); + +router + .route("/:discogsId") + .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Pages(req, "ajouter-un-album/form"); + + await page.getFormAddOne(); + + render(res, page); + } catch (err) { + next(err); + } + }); + +export default router; diff --git a/src/routes/api/v1/albums.js b/src/routes/api/v1/albums.js new file mode 100644 index 0000000..512d6f5 --- /dev/null +++ b/src/routes/api/v1/albums.js @@ -0,0 +1,20 @@ +import express from "express"; +import { ensureLoggedIn } from "connect-ensure-login"; + +import { sendResponse } from "../../../libs/format"; +import Albums from "../../../middleware/Albums"; + +// eslint-disable-next-line new-cap +const router = express.Router(); + +router.route("/").post(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const data = await Albums.postAddOne(req); + + sendResponse(req, res, data); + } catch (err) { + next(err); + } +}); + +export default router; diff --git a/src/routes/api/v1/index.js b/src/routes/api/v1/index.js new file mode 100644 index 0000000..5553c0a --- /dev/null +++ b/src/routes/api/v1/index.js @@ -0,0 +1,22 @@ +import express from "express"; +import { ensureLoggedIn } from "connect-ensure-login"; + +import { sendResponse } from "../../../libs/format"; +import { searchSong } from "../../../helpers"; + +// eslint-disable-next-line new-cap +const router = express.Router(); + +router + .route("/search") + .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const data = await searchSong(req.query.q); + + sendResponse(req, res, data); + } catch (err) { + next(err); + } + }); + +export default router; diff --git a/src/views/error.ejs b/src/views/error.ejs deleted file mode 100644 index 5c36b8a..0000000 --- a/src/views/error.ejs +++ /dev/null @@ -1,22 +0,0 @@ - - - <%- 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 deleted file mode 100644 index 063569a..0000000 --- a/src/views/index.ejs +++ /dev/null @@ -1,27 +0,0 @@ - - - <%- 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 deleted file mode 100644 index 162a205..0000000 --- a/src/views/pages/connexion.ejs +++ /dev/null @@ -1,24 +0,0 @@ -
-
-
-
- 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 deleted file mode 100644 index 4b7e2b6..0000000 --- a/src/views/pages/inscription.ejs +++ /dev/null @@ -1,29 +0,0 @@ -
-
-
-
- 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 deleted file mode 100644 index b929419..0000000 --- a/src/views/partials/footer.ejs +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/views/partials/head.ejs b/src/views/partials/head.ejs deleted file mode 100644 index 448c4ec..0000000 --- a/src/views/partials/head.ejs +++ /dev/null @@ -1,17 +0,0 @@ - - - - - <% 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 deleted file mode 100644 index 0de3caf..0000000 --- a/src/views/partials/header.ejs +++ /dev/null @@ -1,24 +0,0 @@ - \ No newline at end of file diff --git a/views/error.ejs b/views/error.ejs new file mode 100644 index 0000000..ae8b58c --- /dev/null +++ b/views/error.ejs @@ -0,0 +1,11 @@ +
+
+

<%= page.title %>

+ <% if ( errorCode && errorCode === 404 ) { %> + Erreur 404 + <% } %> +
+            <%= page.error %>
+        
+
+
\ No newline at end of file diff --git a/views/index.ejs b/views/index.ejs new file mode 100644 index 0000000..c894cae --- /dev/null +++ b/views/index.ejs @@ -0,0 +1,37 @@ + + + <%- 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/views/pages/ajouter-un-album/form.ejs b/views/pages/ajouter-un-album/form.ejs new file mode 100644 index 0000000..778d75b --- /dev/null +++ b/views/pages/ajouter-un-album/form.ejs @@ -0,0 +1,144 @@ +
+
+
+
+ Miniature +
+ Miniature +
+
    +
  1. {{ track.title }} ({{track.duration}})
  2. +
+
+
+
+
+
+ + +
+ +
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
    +
  1. + {{identifier.value}} ({{identifier.type}}) +
  2. +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
    +
  1. + {{video.title}} +
  2. +
+
+
+
+
    +
  1. + {{extraartist.name}} ({{extraartist.role}}) +
  2. +
+
+
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/views/pages/ajouter-un-album/search.ejs b/views/pages/ajouter-un-album/search.ejs new file mode 100644 index 0000000..23a46b2 --- /dev/null +++ b/views/pages/ajouter-un-album/search.ejs @@ -0,0 +1,105 @@ +
+
+
+
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
PochetteTitrePaysAnnéeFormatGenresStyles
+ + + {{ item.title }} + {{ item.year }}{{ item.country }} +
    +
  • {{ format }}
  • +
+
+
    +
  • {{ genre }}
  • +
+
+
    +
  • {{ style }}
  • +
+
+
+ + \ No newline at end of file diff --git a/views/pages/connexion.ejs b/views/pages/connexion.ejs new file mode 100644 index 0000000..ff1b043 --- /dev/null +++ b/views/pages/connexion.ejs @@ -0,0 +1,30 @@ +
+
+
+
+
+ DarKou +
+

Connexion

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

Pas encore inscrit ? Inscrivez-vous

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

Inscription

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

Déjà inscrit ? Connectez-vous

+
+
+ + +
+
+
+
\ No newline at end of file diff --git a/views/partials/footer.ejs b/views/partials/footer.ejs new file mode 100644 index 0000000..2919c59 --- /dev/null +++ b/views/partials/footer.ejs @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/views/partials/head.ejs b/views/partials/head.ejs new file mode 100644 index 0000000..f8d57a9 --- /dev/null +++ b/views/partials/head.ejs @@ -0,0 +1,39 @@ + + + + + <% if (page.title) { %><%= page.title %> <% } else { %> DarKou - Ma CDThèque <% } %> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/partials/header.ejs b/views/partials/header.ejs new file mode 100644 index 0000000..2af8ee1 --- /dev/null +++ b/views/partials/header.ejs @@ -0,0 +1,49 @@ + \ No newline at end of file