diff --git a/README.md b/README.md index e05aa15..03e3247 100644 --- a/README.md +++ b/README.md @@ -210,22 +210,23 @@ Voici la liste des variables configurables : ``` NODE_ENV # Environnement dans lequel exécuter le projet (development ou production) -PORT # Port sur lequel éxécuter le serveur (par défaut 3001) -MONGODB_URI # Url du serveur mongo (par défaut mongodb://musictopus-db/musictopus) -SECRET # Hash utilisé pour pour sauvegardé les dessions (par défaut waemaeMe5ahc6ce1chaeKohKa6Io8Eik) +PORT # Port sur lequel éxécuter le serveur (3001 par défaut) +MONGODB_URI # Url du serveur mongo (mongodb://musictopus-db/musictopus par défaut) +SECRET # Hash utilisé pour pour sauvegardé les dessions (waemaeMe5ahc6ce1chaeKohKa6Io8Eik par défault) DISCOGS_TOKEN # Token Discogs (vous devez créer un compte sur discogs afin d'en obtenir un gratuitement) FORMSPREE_ID # Id du formulaire formspree pour la page "nous-contacter" MATOMO_URL # Url vers l'instance matomo (exemple: https://analytics.darkou.fr/) MATOMO_ID # Id du site sur votre instance matomo (exemple: 1) -SITE_NAME # Nom du site (utilisé dans le titre des pages) +SITE_NAME # Nom du site utilisé dans le titre des pages (MusicTopus par défaut) AWS_ACCESS_KEY_ID # Clé d'accès AWS AWS_SECRET_ACCESS_KEY # Clé secrète AWS -S3_ENDPOINT # Url de l'instance aws (s3.fr-par.scw.cloud pour scaleway france par exemple) -S3_SIGNATURE # Version de la signature AWS (s3v4 pour scaleway par exemple) -S3_BASEFOLDER # Nom du sous dossier dans lequel seront mis les pochettes des albums -S3_BUCKET # Nom du bucket -JOBS_HEADER_KEY # Nom du header utilisé pour l'identification des tâches cron (par exemple musictopus) -JOBS_HEADER_VALUE # Valeur de la clé +S3_ENDPOINT # Url de l'instance aws (s3.fr-par.scw.cloud par défaut) +S3_SIGNATURE # Version de la signature AWS (s3v4 par défaut) +S3_BASEFOLDER # Nom du sous dossier dans lequel seront mis les pochettes des albums (dev par défaut) +S3_BUCKET # Nom du bucket (musictopus par défaut, à changer impérativement si vous voulez que cela fonctionne) +JOBS_HEADER_KEY # Nom du header utilisé pour l'identification des tâches cron (musictopus par défaut) +JOBS_HEADER_VALUE # Valeur de la clé (ooYee9xok7eigo2shiePohyoGh1eepew par défaut) +REGISTRATION_OPEN # true/false en fonction de si vous souhaitez activer ou non l'inscription à votre instance (true par défaut) ``` ## Contributeurs diff --git a/docker-compose.yml.dev b/docker-compose.yml.dev index dbe38b9..ca86d9e 100644 --- a/docker-compose.yml.dev +++ b/docker-compose.yml.dev @@ -36,6 +36,7 @@ services: S3_SIGNATURE: ${S3_SIGNATURE} JOBS_HEADER_KEY: ${JOBS_HEADER_KEY} JOBS_HEADER_VALUE: ${JOBS_HEADER_VALUE} + REGISTRATION_OPEN: ${REGISTRATION_OPEN} networks: - musictopus musictopus-db: diff --git a/docker-compose.yml.prod b/docker-compose.yml.prod index c343531..6beffa3 100644 --- a/docker-compose.yml.prod +++ b/docker-compose.yml.prod @@ -36,6 +36,7 @@ services: S3_SIGNATURE: ${S3_SIGNATURE} JOBS_HEADER_KEY: ${JOBS_HEADER_KEY} JOBS_HEADER_VALUE: ${JOBS_HEADER_VALUE} + REGISTRATION_OPEN: ${REGISTRATION_OPEN} networks: - musictopus musictopus-db: diff --git a/package.json b/package.json index d574778..49c965d 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,8 @@ "connect-flash": "^0.1.1", "connect-mongo": "^4.6.0", "cookie-parser": "^1.4.6", + "date-fns": "^2.28.0", + "date-fns-tz": "^1.3.3", "debug": "^4.3.3", "disconnect": "^1.2.2", "ejs": "^3.1.6", @@ -55,8 +57,6 @@ "express-session": "^1.17.2", "joi": "^17.6.0", "knacss": "^8.0.4", - "moment": "^2.29.1", - "moment-timezone": "^0.5.34", "mongoose": "^6.2.1", "mongoose-unique-validator": "^3.0.0", "npm-run-all": "^4.1.5", diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..e69de29 diff --git a/sass/collection.scss b/sass/collection.scss index 6412fe1..88f94b5 100644 --- a/sass/collection.scss +++ b/sass/collection.scss @@ -17,6 +17,7 @@ } @include respond-to("small-up") { + width: 33%; &:last-child { padding-right: 0; } @@ -28,6 +29,17 @@ } } + .showMoreFilters { + cursor: pointer; + + .up::before { + transform: rotate(90deg); + } + .down::before { + transform: rotate(270deg); + } + } + .list{ .title { .icon-trash { diff --git a/sass/flash.scss b/sass/flash.scss index db28cb0..fc804a6 100644 --- a/sass/flash.scss +++ b/sass/flash.scss @@ -11,4 +11,12 @@ .header { font-weight: 800; } + + &.info { + background-color: $warning-color; + } + + &.success { + background-color: $success-color; + } } \ No newline at end of file diff --git a/sass/navbar.scss b/sass/navbar.scss index 8e4f3ec..f9583b9 100644 --- a/sass/navbar.scss +++ b/sass/navbar.scss @@ -132,6 +132,8 @@ } &:hover { + background-color: var(--default-color); + .navbar-link { background-color: var(--default-hl-color); color: rgba(0,0,0,.7); @@ -252,6 +254,13 @@ padding-bottom: .5rem; padding-top: .5rem; + hr { + background-color: var(--font-color); + border: none; + height: 2px; + margin: .5rem 0; + } + .navbar-item { cursor: pointer; padding-left: 1.5rem; diff --git a/src/app.js b/src/app.js index c8a9574..85650e8 100644 --- a/src/app.js +++ b/src/app.js @@ -15,6 +15,7 @@ import { isXhr } from "./helpers"; import indexRouter from "./routes"; import maCollectionRouter from "./routes/ma-collection"; +import monCompteRouter from "./routes/mon-compte"; import collectionRouter from "./routes/collection"; import importJobsRouter from "./routes/jobs"; @@ -83,6 +84,7 @@ app.use( ); app.use("/", indexRouter); +app.use("/mon-compte", monCompteRouter); app.use("/ma-collection", maCollectionRouter); app.use("/collection", collectionRouter); app.use("/jobs", importJobsRouter); @@ -97,15 +99,22 @@ app.use((req, res) => { } else { 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, + flash: { + info: req.flash("info"), + error: [ + ...req.flash("error"), + ...(req.session?.flash?.error || []), + ], + success: req.flash("success"), + }, + query: req.query, + params: req.params, + user: req.user, + config, + getBaseUrl: null, + errorCode: 404, }); } }); @@ -122,15 +131,22 @@ app.use((error, req, res, next) => { title: error.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, + flash: { + info: req.flash("info"), + error: [ + ...req.flash("error"), + ...(req.session?.flash?.error || []), + ], + success: req.flash("success"), + }, + query: req.query, + params: req.params, + user: req.user, + config, + getBaseUrl: null, + errorCode: error.errorCode || 500, }); next(); diff --git a/src/config/index.js b/src/config/index.js index 17b341d..7906240 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -17,4 +17,6 @@ module.exports = { jobsHeaderKey: process.env.JOBS_HEADER_KEY || "musictopus", jobsHeaderValue: process.env.JOBS_HEADER_VALUE || "ooYee9xok7eigo2shiePohyoGh1eepew", + registrationOpen: + (process.env.REGISTRATION_OPEN || "true").toLowerCase() === "true", }; diff --git a/src/middleware/Albums.js b/src/middleware/Albums.js index 84f5c5d..2076b32 100644 --- a/src/middleware/Albums.js +++ b/src/middleware/Albums.js @@ -1,4 +1,4 @@ -import moment from "moment"; +import { format as formatDate } from "date-fns"; import Pages from "./Pages"; import Export from "./Export"; @@ -26,7 +26,7 @@ class Albums extends Pages { User: user._id, }; data.released = data.released - ? moment(data.released.replace("-00", "-01")) + ? new Date(data.released.replace("-00", "-01")) : null; delete data.id; @@ -81,6 +81,9 @@ class Albums extends Pages { order = "asc", artists_sort, format, + year, + genre, + style, userId: collectionUserId, } = this.req.query; @@ -94,11 +97,20 @@ class Albums extends Pages { if (format) { where["formats.name"] = format; } + if (year) { + where.year = year; + } + if (genre) { + where.genres = genre; + } + if (style) { + where.styles = style; + } if (!this.req.user && !collectionUserId) { throw new ErrorEvent( 401, - "Cette collection n'est pas publique", + "Collection", "Cette collection n'est pas publique" ); } @@ -114,7 +126,7 @@ class Albums extends Pages { ) { throw new ErrorEvent( 401, - "Cette collection n'est pas publique", + "Collection", "Cette collection n'est pas publique" ); } @@ -199,9 +211,21 @@ class Albums extends Pages { "formats.name", this.req.user._id ); + const years = await Albums.getAllDistincts("year", this.req.user._id); + const genres = await Albums.getAllDistincts( + "genres", + this.req.user._id + ); + const styles = await Albums.getAllDistincts( + "styles", + this.req.user._id + ); this.setPageContent("artists", artists); this.setPageContent("formats", formats); + this.setPageContent("years", years); + this.setPageContent("genres", genres); + this.setPageContent("styles", styles); this.setPageTitle("Ma collection"); } @@ -211,11 +235,16 @@ class Albums extends Pages { async loadItem() { const { itemId: _id } = this.req.params; const { _id: User } = this.req.user; - const item = await AlbumsModel.findOne({ + const album = await AlbumsModel.findOne({ _id, User, }); + const item = { + ...album.toJSON(), + released: formatDate(album.released, "MM/dd/yyyy"), + }; + this.setPageContent("item", item); this.setPageTitle( `Détails de l'album ${item.title} de ${item.artists_sort}` @@ -233,17 +262,24 @@ class Albums extends Pages { if (!user || !user.isPublicCollection) { throw new ErrorEvent( 401, + "Collection non partagée", "Cet utilisateur ne souhaite pas partager sa collection" ); } const artists = await Albums.getAllDistincts("artists_sort", userId); const formats = await Albums.getAllDistincts("formats.name", userId); + const years = await Albums.getAllDistincts("year", userId); + const genres = await Albums.getAllDistincts("genres", userId); + const styles = await Albums.getAllDistincts("styles", userId); this.setPageContent("username", user.username); this.setPageTitle(`Collection publique de ${user.username}`); this.setPageContent("artists", artists); this.setPageContent("formats", formats); + this.setPageContent("years", years); + this.setPageContent("genres", genres); + this.setPageContent("styles", styles); } } diff --git a/src/middleware/Export.js b/src/middleware/Export.js index 380f025..513e440 100644 --- a/src/middleware/Export.js +++ b/src/middleware/Export.js @@ -1,4 +1,5 @@ -import momenttz from "moment-timezone"; +import { utcToZonedTime } from "date-fns-tz"; +import setHours from "date-fns/setHours"; import xl from "excel4node"; class Export { @@ -132,7 +133,9 @@ class Export { } if (released) { ws.cell(currentRow, 7) - .date(momenttz.tz(released, "Europe/Paris").hour(12)) + .date( + setHours(utcToZonedTime(released, "Europe/Paris"), 12) + ) .style({ numberFormat: "dd/mm/yyyy" }); } ws.cell(currentRow, 8).string(format).style(style); diff --git a/src/middleware/Me.js b/src/middleware/Me.js index ae2712c..27a1b46 100644 --- a/src/middleware/Me.js +++ b/src/middleware/Me.js @@ -1,15 +1,12 @@ import Joi from "joi"; import UsersModel from "../models/users"; +import Pages from "./Pages"; /** * Classe permettant la gestion de l'utilisateur connecté */ -class Me { - constructor(req) { - this.req = req; - } - +class Me extends Pages { /** * Méthode permettant de modifier le profil d'un utilisateur * @return {Object} @@ -40,6 +37,33 @@ class Me { return update; } + + /** + * Méthode permettant de modifier le mot de passe d'un utilisateur + */ + async updatePassword() { + const { body } = this.req; + const { _id } = this.req.user; + + const schema = Joi.object({ + oldPassword: Joi.string().required(), + password: Joi.string().required(), + passwordConfirm: Joi.ref("password"), + }); + + const value = await schema.validateAsync(body); + const user = await UsersModel.findById(_id); + + if (!user.validPassword(value.oldPassword)) { + throw new Error("Votre ancien mot de passe n'est pas valide"); + } + + user.salt = value.password; + + await user.save(); + + this.req.flash("success", "Profil correctement mis à jour"); + } } export default Me; diff --git a/src/middleware/Pages.js b/src/middleware/Pages.js index eb3ea75..04c2e2e 100644 --- a/src/middleware/Pages.js +++ b/src/middleware/Pages.js @@ -52,21 +52,20 @@ class Pages { */ render() { this.pageContent.session = this.req.session; - this.pageContent.flashInfo = this.req.flash("info"); - this.pageContent.error = this.req.flash("error") || null; + this.pageContent.flash = { + info: this.req.flash("info"), + error: [ + ...this.req.flash("error"), + ...(this.req.session?.flash?.error || []), + ], + success: this.req.flash("success"), + }; 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(this.req); - 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; } } diff --git a/src/routes/index.js b/src/routes/index.js index ac43a51..faa4142 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -7,6 +7,8 @@ import Auth from "../middleware/Auth"; import render from "../libs/format"; +import { registrationOpen } from "../config"; + // eslint-disable-next-line new-cap const router = express.Router(); @@ -59,11 +61,33 @@ router } ); -router - .route("/inscription") - .get((req, res, next) => { +if (registrationOpen) { + router + .route("/inscription") + .get((req, res, next) => { + try { + const page = new Pages(req, "inscription/index"); + + page.setPageTitle("Inscription"); + + render(res, page); + } catch (err) { + next(err); + } + }) + .post(async (req, res) => { + try { + await Auth.register(req); + + res.redirect("/"); + } catch (err) { + res.redirect("/inscription"); + } + }); +} else { + router.route("/inscription").get((req, res, next) => { try { - const page = new Pages(req, "inscription"); + const page = new Pages(req, "inscription/desactivee"); page.setPageTitle("Inscription"); @@ -71,16 +95,8 @@ router } catch (err) { next(err); } - }) - .post(async (req, res) => { - try { - await Auth.register(req); - - res.redirect("/"); - } catch (err) { - res.redirect("/inscription"); - } }); +} router .route("/ajouter-un-album") diff --git a/src/routes/mon-compte.js b/src/routes/mon-compte.js new file mode 100644 index 0000000..813d0e5 --- /dev/null +++ b/src/routes/mon-compte.js @@ -0,0 +1,35 @@ +import express from "express"; +import { ensureLoggedIn } from "connect-ensure-login"; + +import Me from "../middleware/Me"; + +import render from "../libs/format"; + +// eslint-disable-next-line new-cap +const router = express.Router(); + +router + .route("/") + .get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Me(req, "mon-compte/index"); + + page.setPageTitle("Mon compte"); + + render(res, page); + } catch (err) { + next(err); + } + }) + .post(ensureLoggedIn("/connexion"), async (req, res) => { + try { + const page = new Me(req, "mon-compte/index"); + + await page.updatePassword(); + } catch (err) { + req.flash("error", err.toString()); + } + res.redirect("/mon-compte"); + }); + +export default router; diff --git a/views/error.ejs b/views/error.ejs index 8f80430..520a37e 100644 --- a/views/error.ejs +++ b/views/error.ejs @@ -1,10 +1,8 @@

<%= page.title %>

- <% if ( errorCode && errorCode === 404 ) { %>

Erreur 404

- <% } %> <% if ( process.env.NODE_ENV !== 'production' ) { %>
<%= page.error %>
diff --git a/views/index.ejs b/views/index.ejs index a2a98b5..2386a83 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -83,6 +83,10 @@ - <% if ( page.failureFlash || (error && error.length > 0 ) ) {%> -
- <% if ( page.failureFlash ) {%> -
- Erreur + <% + if ( flash.error.length > 0 ) { + for ( let i = 0 ; i < flash.error.length ; i += 1 ) { + %> +
+
+ Erreur +
+
+ <%= flash.error[i].replace('Error: ', '') %> +
-
- <%= page.failureFlash %> + <% + } + } + if ( flash.info.length > 0 ) { + for ( let i = 0 ; i < flash.info.length ; i += 1 ) { + %> +
+
+ Information +
+
+ <%= flash.info[i] %> +
- <% } %> - <% - if (error && error.length > 0) { - for( let i = 0 ; i < error.length ; i += 1 ) { - %> -
- Erreur -
-
- <%= error %> -
- <% - } - } - %> -
- <% } %> + <% + } + } + if ( flash.success.length > 0 ) { + for ( let i = 0 ; i < flash.success.length ; i += 1 ) { + %> +
+
+ Succès +
+
+ <%= flash.success[i] %> +
+
+ <% + } + } + %> <%- include(viewname) %> diff --git a/views/pages/ajouter-un-album.ejs b/views/pages/ajouter-un-album.ejs index 797bce6..2cd4ab4 100644 --- a/views/pages/ajouter-un-album.ejs +++ b/views/pages/ajouter-un-album.ejs @@ -239,9 +239,9 @@ window.location.href = '/ma-collection'; }) .catch((err) => { - showToastr(err.response?.data?.message || "Impossible d'ajouter ce album pour le moment…"); + showToastr(err.response?.data?.message || "Impossible d'ajouter cet album pour le moment…"); }); }, } }).mount('#app'); - + diff --git a/views/pages/collection.ejs b/views/pages/collection.ejs index da22489..a0530f7 100644 --- a/views/pages/collection.ejs +++ b/views/pages/collection.ejs @@ -40,6 +40,50 @@
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + +
@@ -105,6 +149,7 @@ data() { return { loading: false, + moreFilters: false, items: [], total: 0, page: 1, @@ -112,6 +157,9 @@ limit: 16, artist: '', format: '', + year: '', + genre: '', + style: '', sortOrder: 'artists_sort-asc', sort: 'artists_sort', order: 'asc', @@ -132,6 +180,15 @@ if ( this.format ) { url += `&format=${this.format}`; } + if ( this.year ) { + url += `&year=${this.year}`; + } + if ( this.genre ) { + url += `&genre=${this.genre.replace('&', '%26')}`; + } + if ( this.style ) { + url += `&style=${this.style.replace('&', '%26')}`; + } axios.get(url) .then( response => { @@ -179,6 +236,9 @@ this.fetch(); }, + showMoreFilters() { + this.moreFilters = !this.moreFilters; + } } }).mount('#app'); diff --git a/views/pages/composants.ejs b/views/pages/composants.ejs index 79b8ee6..1cba4e5 100644 --- a/views/pages/composants.ejs +++ b/views/pages/composants.ejs @@ -274,6 +274,22 @@ Ceci est une erreur
+
+
+ Information +
+
+ Ceci est une information +
+
+
+
+ Succès +
+
+ Ceci est un succès +
+
 <div class="flash">
     <div class="header">
diff --git a/views/pages/connexion.ejs b/views/pages/connexion.ejs
index ae162fa..2e86f72 100644
--- a/views/pages/connexion.ejs
+++ b/views/pages/connexion.ejs
@@ -12,9 +12,11 @@
             
         
+ <% if ( config.registrationOpen === true ) { %>

Pas encore inscrit ? Inscrivez-vous

+ <% } %> diff --git a/views/pages/inscription/desactivee.ejs b/views/pages/inscription/desactivee.ejs new file mode 100644 index 0000000..b8140f1 --- /dev/null +++ b/views/pages/inscription/desactivee.ejs @@ -0,0 +1,19 @@ +
+
+

+ Inscription +

+
+
+

+ Les inscriptions sur ce site sont fermées. +

+

+ Vous avez cependant la possibilité d'héberger vous même une version de MusicTopus en vous rendant directement sur le dépot du projet. +

+
+

+ Erreur 404 +

+
+
\ No newline at end of file diff --git a/views/pages/inscription.ejs b/views/pages/inscription/index.ejs similarity index 100% rename from views/pages/inscription.ejs rename to views/pages/inscription/index.ejs diff --git a/views/pages/mon-compte/index.ejs b/views/pages/mon-compte/index.ejs new file mode 100644 index 0000000..d8f7f6a --- /dev/null +++ b/views/pages/mon-compte/index.ejs @@ -0,0 +1,99 @@ +
+

+ Mon compte +

+ +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ + +
+
+
+ + diff --git a/views/pages/mon-compte/ma-collection/index.ejs b/views/pages/mon-compte/ma-collection/index.ejs index 9dd4bbd..eb21c37 100644 --- a/views/pages/mon-compte/ma-collection/index.ejs +++ b/views/pages/mon-compte/ma-collection/index.ejs @@ -6,6 +6,7 @@ Voir ma collection partagée +
@@ -43,6 +44,50 @@
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + +
@@ -154,6 +199,7 @@ data() { return { loading: false, + moreFilters: false, items: [], total: 0, page: 1, @@ -161,6 +207,9 @@ limit: 16, artist: '', format: '', + year: '', + genre: '', + style: '', sortOrder: 'artists_sort-asc', sort: 'artists_sort', order: 'asc', @@ -185,6 +234,15 @@ if ( this.format ) { url += `&format=${this.format}`; } + if ( this.year ) { + url += `&year=${this.year}`; + } + if ( this.genre ) { + url += `&genre=${this.genre.replace('&', '%26')}`; + } + if ( this.style ) { + url += `&style=${this.style.replace('&', '%26')}`; + } axios.get(url) .then( response => { @@ -232,6 +290,9 @@ this.fetch(); }, + showMoreFilters() { + this.moreFilters = !this.moreFilters; + }, toggleModal() { this.showModalDelete = !this.showModalDelete; }, @@ -273,7 +334,7 @@ .finally(() => { this.toggleModalShare(); }); - } + }, } }).mount('#app');