diff --git a/.env.template b/.env.template
index a028982e..15f68f87 100644
--- a/.env.template
+++ b/.env.template
@@ -3,3 +3,10 @@ DATABASE_PASS=escapp
DATABASE_NAME=escapp
DATABASE_HOST=localhost
DATABASE_URL=postgres://escapp:escapp@localhost:5432/escapp
+SMTP_HOST=smtp.resend.com
+SMTP_PORT=465
+SMTP_SECURE=true
+SMTP_USERNAME=resend
+SMTP_PASSWORD=
+SMTP_FROM=noreply@example.com
+UPLOADS_PATH=public/uploads
diff --git a/Dockerfile b/Dockerfile
index 6e482b87..6bd7bdb3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,6 +8,8 @@ RUN npm ci
COPY . .
+RUN mkdir -p public/uploads
+
ENV APP_NAME='localhost'
ENV APP_SSL='false'
ENV DATABASE_USER='escapp'
@@ -15,6 +17,11 @@ ENV DATABASE_PASS='escapp'
ENV DATABASE_NAME='escapp'
ENV DATABASE_HOST='db'
ENV DATABASE_SSL='false'
+ENV SMTP_HOST='smtp.resend.com'
+ENV SMTP_PORT='465'
+ENV SMTP_SECURE='true'
+ENV SMTP_USERNAME='resend'
+ENV UPLOADS_PATH='public/uploads'
ENV NODE_ENV='development'
ENV PORT='3000'
diff --git a/controllers/assets_controller.js b/controllers/assets_controller.js
index 6213352d..d76743e2 100644
--- a/controllers/assets_controller.js
+++ b/controllers/assets_controller.js
@@ -41,7 +41,7 @@ exports.uploadAssets = async (req, res) => {
try {
await attHelper.checksCloudinaryEnv();
// Save the new asset into Cloudinary:
- uploadResult = await attHelper.uploadResource(req.file.path, attHelper.cloudinary_upload_options_zip);
+ uploadResult = await attHelper.uploadResource(req.file.path, attHelper.cloudinary_upload_options_zip, req.file);
} catch (err) {
res.status(500);
res.send(err);
@@ -103,4 +103,3 @@ exports.browse = async (req, res, next) => {
next(err);
}
};
-
diff --git a/controllers/escapeRoom_controller.js b/controllers/escapeRoom_controller.js
index a29964c8..57f08bcb 100644
--- a/controllers/escapeRoom_controller.js
+++ b/controllers/escapeRoom_controller.js
@@ -2,7 +2,6 @@ const Sequelize = require("sequelize");
const {QueryTypes} = require("sequelize");
const sequelize = require("../models");
const {models} = sequelize;
-const cloudinary = require("cloudinary");
const query = require("../queries");
const attHelper = require("../helpers/attachments");
const {nextStep, prevStep} = require("../helpers/progress");
@@ -88,7 +87,7 @@ exports.index = async (req, res, next) => {
} else {
const pageArray = paginate(page, pages, 5);
- res.render("escapeRooms/index.ejs", {escapeRooms, cloudinary, user, count, page, pages, pageArray});
+ res.render("escapeRooms/index.ejs", {escapeRooms, "cloudinary": attHelper, user, count, page, pages, pageArray});
}
} catch (error) {
next(error);
@@ -107,9 +106,9 @@ exports.show = async (req, res) => {
const howManyRetos = await models.retosSuperados.count({"where": {"success": true, "teamId": team.id }});
const finished = howManyRetos === escapeRoom.puzzles.length;
- res.render("escapeRooms/showStudent", {escapeRoom, cloudinary, participant, team, finished});
+ res.render("escapeRooms/showStudent", {escapeRoom, "cloudinary": attHelper, participant, team, finished});
} else {
- res.render("escapeRooms/show", {escapeRoom, cloudinary, hostName, "email": req.session.user.username});
+ res.render("escapeRooms/show", {escapeRoom, "cloudinary": attHelper, hostName, "email": req.session.user.username});
}
};
@@ -142,7 +141,7 @@ exports.create = async (req, res) => {
try {
await attHelper.checksCloudinaryEnv();
- const uploadResult = await attHelper.uploadResource(req.file.path, attHelper.cloudinary_upload_options);
+ const uploadResult = await attHelper.uploadResource(req.file.path, attHelper.cloudinary_upload_options, req.file);
try {
await models.attachment.create({
@@ -219,7 +218,7 @@ exports.update = async (req, res) => {
try {
// Save the new attachment into Cloudinary:
await attHelper.checksCloudinaryEnv();
- const uploadResult = await attHelper.uploadResource(req.file.path, attHelper.cloudinary_upload_options);
+ const uploadResult = await attHelper.uploadResource(req.file.path, attHelper.cloudinary_upload_options, req.file);
// Remember the public_id of the old image.
const old_public_id = er.attachment ? er.attachment.public_id : null;
let attachment = await er.getAttachment();
diff --git a/controllers/hint_controller.js b/controllers/hint_controller.js
index e6acd115..64d52c5e 100644
--- a/controllers/hint_controller.js
+++ b/controllers/hint_controller.js
@@ -103,7 +103,7 @@ exports.pistasUpdate = async (req, res) => {
} else {
try {
await attHelper.checksCloudinaryEnv();
- const uploadResult = await attHelper.uploadResource(req.file.path, attHelper.cloudinary_upload_options_zip);
+ const uploadResult = await attHelper.uploadResource(req.file.path, attHelper.cloudinary_upload_options_zip, req.file);
const old_public_id = escapeRoom.hintApp ? escapeRoom.hintApp.public_id : null; // Update the attachment into the data base.
const att = await escapeRoom.getHintApp();
diff --git a/deploy/.env.template b/deploy/.env.template
index c4532a94..cc0629b9 100644
--- a/deploy/.env.template
+++ b/deploy/.env.template
@@ -3,4 +3,10 @@ DATABASE_NAME=
DATABASE_USER=
DATABASE_PASS=
CLOUDINARY_URL=
-SENDGRID_API_KEY=
+SMTP_HOST=smtp.resend.com
+SMTP_PORT=465
+SMTP_SECURE=true
+SMTP_USERNAME=resend
+SMTP_PASSWORD=
+SMTP_FROM=
+UPLOADS_PATH=public/uploads
diff --git a/helpers/attachments.js b/helpers/attachments.js
index d57d086b..276fe812 100644
--- a/helpers/attachments.js
+++ b/helpers/attachments.js
@@ -3,6 +3,14 @@ const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
+const getUploadPath = () => process.env.UPLOADS_PATH || path.join("public", "uploads");
+
+const safeExtension = (file) => {
+ const extension = path.extname(file && file.originalname || "").toLowerCase();
+
+ return extension.match(/^\.[a-z0-9]+$/) ? extension : "";
+};
+
const uploadResourceToCloudinary = (src, options) => new Promise((resolve, reject) => {
cloudinary.v2.uploader.upload(
src,
@@ -20,33 +28,50 @@ const uploadResourceToCloudinary = (src, options) => new Promise((resolve, rejec
);
});
-const uploadResourceToFileSystem = (src) => new Promise(function (resolve, reject) {
+const uploadResourceToFileSystem = (src, file) => new Promise(function (resolve, reject) {
const salt = `${Math.round(new Date().valueOf() * Math.random())}`;
- const public_id = crypto.createHmac("sha256", salt).update(src).
- digest("hex");
+ const public_id = `${crypto.createHmac("sha256", salt).update(src).
+ digest("hex")}${safeExtension(file)}`;
- const url = path.join("/uploads", public_id);
+ const url = path.posix.join("/uploads", public_id);
+
+ const destination = path.join(getUploadPath(), public_id);
+
+ fs.mkdir(path.dirname(destination), {"recursive": true}, function (dirError) {
+ if (dirError) {
+ reject(dirError);
+ return;
+ }
+ fs.copyFile(src, destination, fs.constants.COPYFILE_EXCL, function (error) {
+ if (error) {
+ reject(error);
+ } else {
+ resolve({
+ public_id,
+ url
+ });
+ }
+ });
+ });
+});
- const destination = path.join("public", "uploads", public_id);
+const deleteResourceFromFileSystem = (public_id) => new Promise(function (resolve) {
+ const destination = path.join(getUploadPath(), public_id);
- fs.copyFile(src, destination, fs.constants.COPYFILE_EXCL, function (error) {
+ fs.unlink(destination, function (error) {
if (error) {
- reject(error);
- } else {
- resolve({
- public_id,
- url
- });
+ console.error("Failed to delete file from filesystem", error);
}
+ resolve();
});
});
-exports.uploadResource = function (src, options) {
+exports.uploadResource = function (src, options, file) {
return new Promise(function (resolve) {
if (process.env.CLOUDINARY_URL) {
resolve(uploadResourceToCloudinary(src, options));
} else {
- resolve(uploadResourceToFileSystem(src));
+ resolve(uploadResourceToFileSystem(src, file));
}
});
};
@@ -61,11 +86,7 @@ exports.deleteResource = async function (public_id, model) {
// Delete from Cloudinary.
cloudinary.api.delete_resources(public_id);
} else {
- const destination = path.join("public", "uploads", public_id); // Delete from local file system.
-
- fs.unlink(destination, function (error) {
- console.error("Failed to delete file from filesystem", error);
- });
+ await deleteResourceFromFileSystem(public_id);
}
};
@@ -110,13 +131,7 @@ exports.video = function (public_id, options) {
"";
};
-exports.checksCloudinaryEnv = () => new Promise((resolve, reject) => {
- if (process.env.CLOUDINARY_URL) {
- resolve();
- } else {
- reject(new Error("The CLOUDINARY_URL env variable is not defined."));
- }
-});
+exports.checksCloudinaryEnv = () => Promise.resolve();
exports.cloudinary_upload_options = {
"folder": "/escapeRoom/attachments",
diff --git a/helpers/mailer.js b/helpers/mailer.js
index fd2d4740..8018295e 100644
--- a/helpers/mailer.js
+++ b/helpers/mailer.js
@@ -1,9 +1,27 @@
-const sgMail = require("@sendgrid/mail");
+const nodemailer = require("nodemailer");
-sgMail.setApiKey(process.env.SENDGRID_API_KEY);
+const getSmtpPort = () => parseInt(process.env.SMTP_PORT || "465", 10);
+
+const isSmtpSecure = () => {
+ if (process.env.SMTP_SECURE === undefined) {
+ return getSmtpPort() === 465;
+ }
+
+ return process.env.SMTP_SECURE === "true";
+};
+
+const getTransporter = () => nodemailer.createTransport({
+ "host": process.env.SMTP_HOST || "smtp.resend.com",
+ "port": getSmtpPort(),
+ "secure": isSmtpSecure(),
+ "auth": {
+ "user": process.env.SMTP_USERNAME || "resend",
+ "pass": process.env.SMTP_PASSWORD
+ }
+});
exports.resetPasswordEmail = (to, subject = "escapp", text, html) => {
- const from = `noreply@${process.env.APP_NAME || "escapp.dit.upm.es"}`;
+ const from = process.env.SMTP_FROM || `noreply@${process.env.APP_NAME || "escapp.dit.upm.es"}`;
const msg = {
to,
from,
@@ -12,6 +30,5 @@ exports.resetPasswordEmail = (to, subject = "escapp", text, html) => {
html
};
- return sgMail.send(msg);
+ return getTransporter().sendMail(msg);
};
-
diff --git a/helpers/utils.js b/helpers/utils.js
index d9e013bc..cf107940 100644
--- a/helpers/utils.js
+++ b/helpers/utils.js
@@ -3,7 +3,7 @@ const {Op} = Sequelize;
const sequelize = require("../models");
const {models} = sequelize;
const {nextStep, prevStep} = require("./progress");
-const cloudinary = require("cloudinary");
+const attHelper = require("../helpers/attachments");
const ejs = require("ejs");
const queries = require("../queries");
const {OK, NOT_A_PARTICIPANT, PARTICIPANT, NOK, NOT_ACTIVE, NOT_STARTED, TOO_LATE, AUTHOR, ERROR} = require("../helpers/apiCodes");
@@ -54,7 +54,7 @@ exports.playInterface = async (name, req, res, next) => {
if (isAdmin || isAuthor) {
res.render("escapeRooms/play/play", {
"escapeRoom": req.escapeRoom,
- cloudinary,
+ "cloudinary": attHelper,
"team": {
"turno": req.turn,
"retos": []
@@ -113,7 +113,7 @@ exports.playInterface = async (name, req, res, next) => {
await exports.automaticallySetAttendance(team, req.session.user.id, req.escapeRoom.automaticAttendance);
const hints = await models.requestedHint.findAll({"where": {"teamId": team.id, "success": true}, "include": [{"model": models.hint, "include": [{"model": models.puzzle, "attributes": ["order"]}]}], "order": [["createdAt", "ASC"]]});
- res.render("escapeRooms/play/play", {"escapeRoom": req.escapeRoom, "hostName": process.env.APP_NAME ? `https://${process.env.APP_NAME}` : "http://localhost:3000", cloudinary, "teams": [], team, token, "userId": req.session.user.id, "turnoId": team.turno.id, "teamId": team.id, "isStudent": true, "hints": hints || [], "endPoint": name, "layout": false });
+ res.render("escapeRooms/play/play", {"escapeRoom": req.escapeRoom, "hostName": process.env.APP_NAME ? `https://${process.env.APP_NAME}` : "http://localhost:3000", "cloudinary": attHelper, "teams": [], team, token, "userId": req.session.user.id, "turnoId": team.turno.id, "teamId": team.id, "isStudent": true, "hints": hints || [], "endPoint": name, "layout": false });
} catch (err) {
next(err);
}
diff --git a/package-lock.json b/package-lock.json
index 8e79afc0..d3a05dcf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,6 @@
"name": "escapp",
"version": "1.0.0",
"dependencies": {
- "@sendgrid/mail": "^8.1.4",
"body-parser": "^1.19.2",
"cloudinary": "^1.28.1",
"compression": "^1.7.4",
@@ -1447,41 +1446,6 @@
"node": ">=14"
}
},
- "node_modules/@sendgrid/client": {
- "version": "8.1.6",
- "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.6.tgz",
- "integrity": "sha512-/BHu0hqwXNHr2aLhcXU7RmmlVqrdfrbY9KpaNj00KZHlVOVoRxRVrpOCabIB+91ISXJ6+mLM9vpaVUhK6TwBWA==",
- "dependencies": {
- "@sendgrid/helpers": "^8.0.0",
- "axios": "^1.12.0"
- },
- "engines": {
- "node": ">=12.*"
- }
- },
- "node_modules/@sendgrid/helpers": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz",
- "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==",
- "dependencies": {
- "deepmerge": "^4.2.2"
- },
- "engines": {
- "node": ">= 12.0.0"
- }
- },
- "node_modules/@sendgrid/mail": {
- "version": "8.1.6",
- "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.6.tgz",
- "integrity": "sha512-/ZqxUvKeEztU9drOoPC/8opEPOk+jLlB2q4+xpx6HVLq6aFu3pMpalkTpAQz8XfRfpLp8O25bh6pGPcHDCYpqg==",
- "dependencies": {
- "@sendgrid/client": "^8.1.5",
- "@sendgrid/helpers": "^8.0.0"
- },
- "engines": {
- "node": ">=12.*"
- }
- },
"node_modules/@sinclair/typebox": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
@@ -1888,16 +1852,6 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz",
"integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="
},
- "node_modules/axios": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
- "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
- "dependencies": {
- "follow-redirects": "^1.15.11",
- "form-data": "^4.0.5",
- "proxy-from-env": "^2.1.0"
- }
- },
"node_modules/babel-jest": {
"version": "28.1.3",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz",
@@ -2753,6 +2707,7 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3699,25 +3654,6 @@
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="
},
- "node_modules/follow-redirects": {
- "version": "1.16.0",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
- "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/RubenVerborgh"
- }
- ],
- "engines": {
- "node": ">=4.0"
- },
- "peerDependenciesMeta": {
- "debug": {
- "optional": true
- }
- }
- },
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -3752,21 +3688,6 @@
"node": "*"
}
},
- "node_modules/form-data": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
- "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "es-set-tostringtag": "^2.1.0",
- "hasown": "^2.0.2",
- "mime-types": "^2.1.12"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/formidable": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz",
@@ -6353,14 +6274,6 @@
"node": ">= 0.10"
}
},
- "node_modules/proxy-from-env": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
- "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
diff --git a/package.json b/package.json
index cabcf38c..e7bb1c03 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,6 @@
"unused": "unused-exports --path ./ --ignore node_modules/ --ignore public --ignore migrations"
},
"dependencies": {
- "@sendgrid/mail": "^8.1.4",
"body-parser": "^1.19.2",
"cloudinary": "^1.28.1",
"compression": "^1.7.4",
diff --git a/views/attachments/_show.ejs b/views/attachments/_show.ejs
index 607cf483..14f65d1f 100644
--- a/views/attachments/_show.ejs
+++ b/views/attachments/_show.ejs
@@ -7,11 +7,11 @@
<% } else if ( attachment.mime === 'video/mp4') { %>
<%- cloudinary.video(locals.attachment.public_id, {controls: false, secure: true})%>
<% } else if ( attachment.mime === 'application/pdf') { %>
- <%- cloudinary.image(locals.attachment.public_id, {format: 'jpg', secure: true}) %>
+ <%= locals.attachment.filename %>
<% } else { %>
<% } %>
<% } else { %>
-<% } %>
\ No newline at end of file
+<% } %>