diff --git a/.eslintrc.js b/.eslintrc.js index 1f95ab5..dc91020 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,7 +22,7 @@ module.exports = { camelcase: [ "error", { - allow: ["artists_sort"], + allow: ["artists_sort", "access_token", "api_url", "media_ids"], }, ], }, diff --git a/javascripts/ajouter-un-album.js b/javascripts/ajouter-un-album.js index eb55697..2a2ce2c 100644 --- a/javascripts/ajouter-un-album.js +++ b/javascripts/ajouter-un-album.js @@ -9,6 +9,7 @@ Vue.createApp({ items: [], details: {}, modalIsVisible: false, + submitting: false, formats: [ "Vinyl", "Acetate", @@ -160,12 +161,18 @@ Vue.createApp({ }); }, add() { - axios + if (this.submitting) { + return true; + } + this.submitting = true; + + return axios .post("/api/v1/albums", this.details) .then(() => { window.location.href = "/ma-collection"; }) .catch((err) => { + this.submitting = false; showToastr( err.response?.data?.message || "Impossible d'ajouter cet album pour le moment…" diff --git a/javascripts/main.js b/javascripts/main.js index 11daeb8..08e5677 100644 --- a/javascripts/main.js +++ b/javascripts/main.js @@ -1,6 +1,8 @@ /* eslint-disable no-unused-vars */ const { protocol, host } = window.location; +let timeout = null; + /** * Fonction permettant d'afficher un message dans un toastr * @param {String} message @@ -11,12 +13,23 @@ function showToastr(message, success = false) { x.getElementsByTagName("SPAN")[0].innerHTML = message; } - x.className = `${x.className} show`.replace("sucess", ""); - if (success) { - x.className = `${x.className} success`; + if (timeout) { + clearTimeout(timeout); + x.classList.remove("show"); } - setTimeout(() => { - x.className = x.className.replace("show", ""); + + x.classList.remove("success"); + x.classList.remove("error"); + if (success) { + x.classList.add("success"); + } else { + x.classList.add("error"); + } + + x.classList.add("show"); + + timeout = setTimeout(() => { + x.classList.remove("show"); }, 3000); } diff --git a/javascripts/mon-compte/index.js b/javascripts/mon-compte/index.js index ed40f03..6faa615 100644 --- a/javascripts/mon-compte/index.js +++ b/javascripts/mon-compte/index.js @@ -2,27 +2,100 @@ if (typeof email !== "undefined" && typeof username !== "undefined") { Vue.createApp({ data() { return { - // eslint-disable-next-line no-undef - email, - // eslint-disable-next-line no-undef - username, - oldPassword: "", - password: "", - passwordConfirm: "", + formData: { + // eslint-disable-next-line no-undef + email, + // eslint-disable-next-line no-undef + username, + oldPassword: "", + password: "", + passwordConfirm: "", + // eslint-disable-next-line no-undef + mastodon: mastodon || { + publish: false, + url: "", + token: "", + message: + "Je viens d'ajouter {artist} - {album} à ma collection !", + }, + }, loading: false, + errors: [], }; }, methods: { // eslint-disable-next-line no-unused-vars - async updateProfil(event) { - // try { - // if ( this.password !== this.passwordConfirm ) { - // throw "La confirnation du mot de passe ne correspond pas"; - // } - // } catch(err) { - // event.preventDefault(); - // showToastr(err); - // } + async testMastodon() { + const { url, token } = this.formData.mastodon; + + if (!url) { + this.errors.push("emptyUrl"); + } + if (!token) { + this.errors.push("emptyToken"); + } + + if (this.errors.length > 0) { + return false; + } + + try { + await axios.post(`/api/v1/mastodon`, { url, token }); + + showToastr("Configuration valide !", true); + } catch (err) { + showToastr( + err.response?.data?.message || + "Impossible de tester cette configuration", + false + ); + } + + return true; + }, + // eslint-disable-next-line no-unused-vars + async updateProfil() { + this.errors = []; + const { oldPassword, password, passwordConfirm, mastodon } = + this.formData; + + if (password && !oldPassword) { + this.errors.push("emptyPassword"); + } + + if (password !== passwordConfirm) { + this.errors.push("passwordsDiffer"); + } + + if (this.errors.length > 0) { + return false; + } + + this.loading = true; + + const data = { + mastodon, + }; + + if (password) { + data.password = password; + data.oldPassword = oldPassword; + } + + try { + await axios.patch(`/api/v1/me`, data); + + showToastr("Profil mis à jour", true); + } catch (err) { + showToastr( + err.response?.data?.message || + "Impossible de mettre à jour votre profil" + ); + } + + this.loading = false; + + return true; }, }, }).mount("#mon-compte"); diff --git a/package.json b/package.json index a2f9a74..bae29cc 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "gulp-uglify": "^3.0.2", "joi": "^17.6.0", "knacss": "^8.0.4", + "mastodon": "^1.2.2", "mongoose": "^6.2.1", "mongoose-unique-validator": "^3.0.0", "nodemailer": "^6.7.8", diff --git a/sass/colors.scss b/sass/colors.scss index 83b5325..af22cce 100644 --- a/sass/colors.scss +++ b/sass/colors.scss @@ -22,10 +22,12 @@ $nord15: #b48ead; $primary-color: $nord8; $danger-color: $nord11; +$error-color: $nord12; $warning-color: $nord13; $success-color: $nord14; $primary-color-hl: darken($primary-color, $hoverAmount); $danger-color-hl: darken($danger-color, $hoverAmount); +$error-color-hl: darken($error-color, $hoverAmount); $warning-color-hl: darken($warning-color, $hoverAmount); $success-color-hl: darken($success-color, $hoverAmount); diff --git a/sass/error.scss b/sass/error.scss index cf534ac..930d6db 100644 --- a/sass/error.scss +++ b/sass/error.scss @@ -1,4 +1,6 @@ -.error { - min-height: calc(100vh - 3.25rem - 100px); - padding-top: 4rem; +main { + &.error { + min-height: calc(100vh - 3.25rem - 100px); + padding-top: 4rem; + } } \ No newline at end of file diff --git a/sass/index.scss b/sass/index.scss index 1979bb1..dde6a19 100644 --- a/sass/index.scss +++ b/sass/index.scss @@ -42,6 +42,7 @@ @import './loader'; @import './error'; +@import './messages.scss'; @import './500'; @import './home'; @import './ajouter-un-album'; diff --git a/sass/messages.scss b/sass/messages.scss new file mode 100644 index 0000000..7633697 --- /dev/null +++ b/sass/messages.scss @@ -0,0 +1,9 @@ +.message { + margin: 8px 0; + padding: 0; + font-size: 0.8rem; + + &.error { + color: $error-color-hl; + } +} \ No newline at end of file diff --git a/sass/toast.scss b/sass/toast.scss index 46777e7..f2434f8 100644 --- a/sass/toast.scss +++ b/sass/toast.scss @@ -3,16 +3,19 @@ min-width: 250px; max-width: 360px; position: fixed; - z-index: 31; + z-index: 66; right: 30px; top: 30px; font-size: 17px; padding: 1.25rem 2.5rem 1.25rem 1.5rem; - background-color: $danger-color; - color: $button-alternate-color; border-radius: 6px; + &.error { + background-color: $danger-color; + color: $button-alternate-color; + } + &.success { background-color: $success-color; color: $button-font-color; diff --git a/src/app.js b/src/app.js index 1cbd1bf..91794bc 100644 --- a/src/app.js +++ b/src/app.js @@ -22,6 +22,7 @@ import importJobsRouter from "./routes/jobs"; import importAlbumRouterApiV1 from "./routes/api/v1/albums"; import importSearchRouterApiV1 from "./routes/api/v1/search"; +import importMastodonRouterApiV1 from "./routes/api/v1/mastodon"; import importMeRouterApiV1 from "./routes/api/v1/me"; import importContactRouterApiV1 from "./routes/api/v1/contact"; @@ -84,6 +85,7 @@ app.use("/collection", collectionRouter); app.use("/jobs", importJobsRouter); app.use("/api/v1/albums", importAlbumRouterApiV1); app.use("/api/v1/search", importSearchRouterApiV1); +app.use("/api/v1/mastodon", importMastodonRouterApiV1); app.use("/api/v1/me", importMeRouterApiV1); app.use("/api/v1/contact", importContactRouterApiV1); diff --git a/src/middleware/Albums.js b/src/middleware/Albums.js index 943eaec..5873c7f 100644 --- a/src/middleware/Albums.js +++ b/src/middleware/Albums.js @@ -1,5 +1,9 @@ import { format as formatDate } from "date-fns"; +import fs from "fs"; +import Mastodon from "mastodon"; +import { v4 } from "uuid"; +import axios from "axios"; import Pages from "./Pages"; import Export from "./Export"; @@ -40,9 +44,83 @@ class Albums extends Pages { id: album._id, }; - const job = new JobsModel(jobData); + try { + const User = await UsersModel.findOne({ _id: user._id }); - job.save(); + const { mastodon: mastodonConfig } = User; + + const { publish, token, url, message } = mastodonConfig; + + if (publish && url && token) { + const M = new Mastodon({ + access_token: token, + api_url: url, + }); + + const video = + data.videos && data.videos.length > 0 + ? data.videso[0].uri + : ""; + + const status = `${( + message || + "Je viens d'ajouter {artist} - {album} à ma collection !" + ) + .replaceAll("{artist}", data.artists[0].name) + .replaceAll("{format}", data.formats[0].name) + .replaceAll("{year}", data.year) + .replaceAll("{video}", video) + .replaceAll("{album}", data.title)} + +Publié automatiquement via #musictopus`; + + const media_ids = []; + + if (data.images.length > 0) { + for (let i = 0; i < data.images.length; i += 1) { + if (media_ids.length === 4) { + break; + } + + const filename = `${v4()}.jpg`; + const file = `/tmp/${filename}`; + + // eslint-disable-next-line no-await-in-loop + const { data: buff } = await axios.get( + data.images[i].uri, + { + responseType: "arraybuffer", + } + ); + + fs.writeFileSync(file, buff); + + // eslint-disable-next-line no-await-in-loop + const { data: media } = await M.post("media", { + file: fs.createReadStream(file), + }); + + const { id } = media; + + media_ids.push(id); + + fs.unlinkSync(file); + } + } + + await M.post("statuses", { status, media_ids }); + } + + const job = new JobsModel(jobData); + + job.save(); + } catch (err) { + throw new ErrorEvent( + 500, + "Mastodon", + "Album ajouté à votre collection mais impossible de publier sur Mastodon" + ); + } return album; } diff --git a/src/middleware/Me.js b/src/middleware/Me.js index 27a1b46..4d82599 100644 --- a/src/middleware/Me.js +++ b/src/middleware/Me.js @@ -12,21 +12,41 @@ class Me extends Pages { * @return {Object} */ async patchMe() { - const { body, user } = this.req; + const { body } = this.req; + const { _id } = this.req.user; const schema = Joi.object({ isPublicCollection: Joi.boolean(), + oldPassword: Joi.string(), + password: Joi.string(), + passwordConfirm: Joi.ref("password"), + mastodon: { + publish: Joi.boolean(), + url: Joi.string().uri().allow(null, ""), + token: Joi.string().allow(null, ""), + message: Joi.string().allow(null, ""), + }, }); const value = await schema.validateAsync(body); - const update = await UsersModel.findByIdAndUpdate( - user._id, - { $set: value }, - { new: true } - ); + const user = await UsersModel.findById(_id); + + if (value.oldPassword) { + if (!user.validPassword(value.oldPassword)) { + throw new Error("Votre ancien mot de passe n'est pas valide"); + } + } + + user.mastodon = value.mastodon; + + if (value.password) { + user.salt = value.password; + } + + user.save(); await new Promise((resolve, reject) => { - this.req.login(update, (err) => { + this.req.login(user, (err) => { if (err) { return reject(err); } @@ -35,34 +55,7 @@ class Me extends Pages { }); }); - 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"); + return user; } } diff --git a/src/models/users.js b/src/models/users.js index 96629c0..b8a047a 100644 --- a/src/models/users.js +++ b/src/models/users.js @@ -29,6 +29,12 @@ const UserSchema = new mongoose.Schema( type: Boolean, default: false, }, + mastodon: { + publish: Boolean, + token: String, + url: String, + message: String, + }, }, { timestamps: true, diff --git a/src/routes/api/v1/mastodon.js b/src/routes/api/v1/mastodon.js new file mode 100644 index 0000000..87df23d --- /dev/null +++ b/src/routes/api/v1/mastodon.js @@ -0,0 +1,28 @@ +import express from "express"; +import { ensureLoggedIn } from "connect-ensure-login"; + +import Mastodon from "mastodon"; +import { sendResponse } from "../../../libs/format"; + +// eslint-disable-next-line new-cap +const router = express.Router(); + +router.route("/").post(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const { url, token } = req.body; + + const M = new Mastodon({ + access_token: token, + api_url: url, + }); + + const data = await M.post("statuses", { + status: "Test d'intégration de Mastodon sur mon compte #musictopus 👌", + }); + return sendResponse(req, res, data); + } catch (err) { + return next(err); + } +}); + +export default router; diff --git a/src/routes/mon-compte.js b/src/routes/mon-compte.js index 813d0e5..577e4e5 100644 --- a/src/routes/mon-compte.js +++ b/src/routes/mon-compte.js @@ -8,28 +8,16 @@ 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"); +router.route("/").get(ensureLoggedIn("/connexion"), async (req, res, next) => { + try { + const page = new Me(req, "mon-compte/index"); - page.setPageTitle("Mon compte"); + 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"); - }); + render(res, page); + } catch (err) { + next(err); + } +}); export default router; diff --git a/views/pages/ajouter-un-album.ejs b/views/pages/ajouter-un-album.ejs index 2141963..cfafca1 100644 --- a/views/pages/ajouter-un-album.ejs +++ b/views/pages/ajouter-un-album.ejs @@ -180,7 +180,7 @@ diff --git a/views/pages/mon-compte/index.ejs b/views/pages/mon-compte/index.ejs index af7b634..ca90b9b 100644 --- a/views/pages/mon-compte/index.ejs +++ b/views/pages/mon-compte/index.ejs @@ -3,75 +3,142 @@ Mon compte -
-
- -
- - + +
+
+

Mes données personnelles

+
+
+ + +
+
+ + +
+
+ + +
+ Pour changer votre mot de passe vous devez saisir votre mot de passe actuel +
+
+
+ + +
+
+ + +
+ La confirmation ne correspond pas avec votre nouveau mot de passe +
+
+
-
- - -
-
- - -
-
-
- - -
-
- - +
+

Mon activité

+
+
+ + + +
+
+ + +
+
+ + +
+
+ + + + Variables possibles : +
    +
  • {artist}, exemple : Iron Maiden
  • +
  • {album}, exemple : Powerslave
  • +
  • {format}, exemple : Cassette
  • +
  • {year}, exemple: 1984
  • +
  • {video}, exemple : https://www.youtube.com/watch?v=Qx0s8OqgBIw
  • +
+
+
+ +
- -
+ +
+