diff --git a/app.js b/app.js index 4bf00c2..d3b36f2 100644 --- a/app.js +++ b/app.js @@ -32,6 +32,7 @@ app.set("view engine", "ejs"); passportInit(); app.use( session({ + proxy: true, secret: process.env.SECRET_KEY, resave: false, saveUninitialized: true, diff --git a/controllers/index.js b/controllers/index.js index daeec93..3f10aaa 100644 --- a/controllers/index.js +++ b/controllers/index.js @@ -1,13 +1,26 @@ const Message = require("../models/message.js"); +const User = require("../models/user.js"); const asyncHandler = require("express-async-handler"); exports.index = asyncHandler(async (req, res, next) => { // if user is logged in if (req.user) { // gets messages addressed to user from database - const DMs = await Message.find({ to: req.user.username }).lean().exec(); + let DMs = await Message.find({ to: req.user.username }).lean().exec(); + // replaces from field with actual user + for (let i = 0; i < DMs.length; i++) { + const user = await User.findOne({ username: DMs[i].from }).lean().exec(); + DMs[i].from = user; + } // gets messages addressed to all chat from database const allChat = await Message.find({ to: "all" }).lean().exec(); + // replaces from field with actual user + for (let z = 0; z < allChat.length; z++) { + const user = await User.findOne({ username: allChat[z].from }) + .lean() + .exec(); + allChat[z].from = user; + } // render index page return res.render("index", { DMs: DMs, diff --git a/controllers/message.js b/controllers/message.js index abb6bd7..9a9ce8f 100644 --- a/controllers/message.js +++ b/controllers/message.js @@ -1,4 +1,5 @@ const Message = require("../models/message.js"); +const User = require("../models/user.js"); const asyncHandler = require("express-async-handler"); const { body, validationResult } = require("express-validator"); const { default: mongoose } = require("mongoose"); @@ -7,6 +8,7 @@ exports.new_get = (req, res, next) => { res.render("newmsg", { user: req.user, errors: false, + to: false, }); }; @@ -26,6 +28,7 @@ exports.new_post = [ res.render("newmsg", { errors: errors.array(), user: req.user, + to: false, }); return; } @@ -49,9 +52,20 @@ exports.new_post = [ }), ]; +exports.respond_get = (req, res, next) => { + res.render("newmsg", { + user: req.user, + errors: false, + to: req.params.recipient, + }); +}; + exports.get = asyncHandler(async (req, res, next) => { // find message - const message = await Message.findById(req.params.messageID).lean().exec(); + let message = await Message.findById(req.params.messageID).lean().exec(); + // replace from field with actual user + const user = await User.findOne({ username: message.from }).lean().exec(); + message.from = user; res.render("message", { message: message, user: req.user, diff --git a/controllers/user.js b/controllers/user.js index d576915..d04a268 100644 --- a/controllers/user.js +++ b/controllers/user.js @@ -1,15 +1,25 @@ +const fs = require("fs"); const User = require("../models/user.js"); const asyncHandler = require("express-async-handler"); const { body, validationResult } = require("express-validator"); const { default: mongoose } = require("mongoose"); const bcrypt = require("bcryptjs"); const passport = require("passport"); +const multer = require("multer"); +const storage = multer.memoryStorage(); +const upload = multer({ + storage: storage, + limits: { fileSize: 1840000 }, +}); exports.new_get = (req, res, next) => { res.render("newuser", { errors: false }); }; exports.new_post = [ + // this goes first as it replaces bodyparser to some extent + upload.single("avatar"), + // Validate and sanitize fields body("username", "Please enter a username!") .trim() @@ -48,14 +58,28 @@ exports.new_post = [ }); return; } + if (!req.file) { + const error = { + msg: "Avatar not uploaded", + }; + // add to errors array + errArr = [error]; + // Render the creation form again with error message. + res.render("newuser", { + errors: errArr, + }); + return; + } // else data is valid // create new user with hashed password const user = new User({ username: req.body.username, password: await bcrypt.hash(req.body.password, 10), _id: new mongoose.Types.ObjectId(), + avatar: req.file.buffer, }); + // save to mongo await user.save(); // saved. Redirect to login page @@ -99,34 +123,37 @@ exports.put_get = asyncHandler(async (req, res, next) => { const user = await User.findById(req.params.userID).lean().exec(); res.render("edituser", { user: user, + errors: false, }); }); exports.put = [ + // this goes first as it replaces bodyparser to some extent + upload.single("avatar"), + // Validate and sanitize fields // username cannot be changed for security/impersonation reasons body("oldpassword", "Please enter current password to authorize changes!") .trim() .escape() - .isLength({ min: 1 }), + .isLength({ min: 1 }) + .custom(async (value, { req }) => { + const dbUser = await User.findById(req.params.userID).lean().exec(); + const match = await bcrypt.compare(value, dbUser.password); + if (!match) { + return Promise.reject(); + } + }) + .withMessage("Incorrect password!"), body("password", "Please enter new password!").optional().trim().escape(), - body("confirm", "Please confirm new password!") - .optional() - .trim() - .escape() - .custom((value, { req }) => { - if (value !== req.body.password) - throw new Error("Passwords don't match!"); - else return value; - }), - - // tbd: allow profile pic changes with multer - // Process request after validation and sanitization. asyncHandler(async (req, res, next) => { + // find existing user in DB + const dbUser = await User.findById(req.params.userID).lean().exec(); + // Extract the validation errors from a request. const errors = validationResult(req); // if there are validation errors @@ -134,30 +161,88 @@ exports.put = [ // Render the creation form again with sanitized values/error messages. res.render("edituser", { errors: errors.array(), + user: dbUser, }); return; } // else data is valid - // find existing user in DB - const dbUser = User.findById(req.params.userID).lean().exec(); - // make new user - const user = new User({ - username: dbUser.username, - password: await bcrypt.hash(req.body.password, 10), - _id: dbUser._id, - }); + // ascertain which fields have been changed by the user + let password = (image = false); + let user = null; - // update user in DB to new user - await User.findByIdAndUpdate(dbUser._id, user, {}); + // check for password field + if ( + req.body.password && + req.body.password != "" && + req.body.password != "null" && + req.body.password !== null + ) { + password = true; + } + // check for image + if (req.file) image = true; - // saved. logout and redirect to log in again - req.logout((err) => { - if (err) { - return next(err); + // if there is a password + if (password) { + // if there is an image + if (image) { + // make new user with new avatar and password + user = new User({ + username: dbUser.username, + password: await bcrypt.hash(req.body.password, 10), + _id: dbUser._id, + avatar: req.file.buffer, + }); } - res.redirect("/user/login"); + // if there is not an image + else { + // make new user with new password only + user = new User({ + username: dbUser.username, + password: await bcrypt.hash(req.body.password, 10), + _id: dbUser._id, + avatar: dbUser.avatar, + }); + } + } + // if there is not a password but there is an image + else if (image) { + // make new user with new image only + user = new User({ + username: dbUser.username, + password: dbUser.password, + _id: dbUser._id, + avatar: req.file.buffer, + }); + } + // if nothing was passed, user stays null + + // update user in DB to new user if needed + if (user != null) { + await User.findByIdAndUpdate(dbUser._id, user, {}); + // saved. logout and redirect to log in again + req.logout((err) => { + if (err) { + return next(err); + } + res.redirect("/user/login"); + }); + return; + } + + // if nothing updated, redirect to edit page with error + const error = { + msg: "Nothing updated", + }; + // add to errors array + errArr = [error]; + // render error + res.render("edituser", { + user: dbUser, + errors: errArr, }); + return; }), ]; diff --git a/models/user.js b/models/user.js index 59b65b1..94d13fe 100644 --- a/models/user.js +++ b/models/user.js @@ -5,6 +5,7 @@ const UserSchema = new Schema({ username: { type: String, required: true }, password: { type: String, required: true }, _id: { type: mongoose.ObjectId, required: true }, + avatar: { type: Buffer, required: true }, }); // Virtual for user URL diff --git a/package-lock.json b/package-lock.json index 7eb4a73..4349434 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "express-session": "^1.17.3", "express-validator": "^7.0.1", "mongoose": "^7.5.1", + "multer": "^1.4.5-lts.1", "passport": "^0.6.0", "passport-local": "^1.0.0", "passport-local-mongoose": "^8.0.0" @@ -157,6 +158,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -246,6 +252,22 @@ "node": ">=14.20.1" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -311,6 +333,20 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -343,6 +379,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -858,6 +899,11 @@ "node": ">=0.12.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/jake": { "version": "10.8.7", "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", @@ -991,6 +1037,25 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mongodb": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.8.1.tgz", @@ -1111,6 +1176,23 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1119,6 +1201,14 @@ "node": ">= 0.6" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -1232,6 +1322,11 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1320,6 +1415,25 @@ "resolved": "https://registry.npmjs.org/read-input/-/read-input-0.3.1.tgz", "integrity": "sha512-J1ZkWCnB4altU7RTe+62PSfa21FrEtfKyO9fuqR3yP8kZku3nIwaw2Krj383JC7egAIl5Zyz2w+EOu9uXH5HZw==" }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1503,6 +1617,27 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -1588,6 +1723,11 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -1607,6 +1747,11 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/package.json b/package.json index 5a2f909..162a153 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "express-session": "^1.17.3", "express-validator": "^7.0.1", "mongoose": "^7.5.1", + "multer": "^1.4.5-lts.1", "passport": "^0.6.0", "passport-local": "^1.0.0", "passport-local-mongoose": "^8.0.0" diff --git a/routes/message.js b/routes/message.js index 5f726d8..23f1f3f 100644 --- a/routes/message.js +++ b/routes/message.js @@ -6,6 +6,8 @@ const message_controller = require("../controllers/message.js"); router.get("/new", message_controller.new_get); router.post("/new", message_controller.new_post); // C +router.get("/new/:recipient", message_controller.respond_get); + // message functions router.get("/:messageID", message_controller.get); // R diff --git a/views/editmsg.ejs b/views/editmsg.ejs index 6046d07..294d6cc 100644 --- a/views/editmsg.ejs +++ b/views/editmsg.ejs @@ -15,7 +15,14 @@ type="button" class="btn btn-outline-light px-4 py-2" > - <%= user.username %> +
+ + <%= user.username %> +
diff --git a/views/edituser.ejs b/views/edituser.ejs index d636ec1..41ed2f8 100644 --- a/views/edituser.ejs +++ b/views/edituser.ejs @@ -6,6 +6,9 @@ + <% if (errors) { errors.forEach(error => { %> +

<%= error.msg %>

+ <% }) } %>
@@ -15,15 +18,27 @@ type="button" class="btn btn-outline-light px-4 py-2" > - <%= user.username %> +
+ + <%= user.username %> +

Editing <%= user.username %>

-
+

Required fields are labeled *

+
Current Password:Current Password*:
- New Password (optional): + New Password:
- Confirm New Password (optional): + New Avatar:
diff --git a/views/user.ejs b/views/user.ejs index 9d03e88..b9bd109 100644 --- a/views/user.ejs +++ b/views/user.ejs @@ -15,12 +15,26 @@ type="button" class="btn btn-outline-light px-4 py-2" > - <%= currentUser.username %> +
+ + <%= currentUser.username %> +
-

<%= user.username %>

-

One day there will be avatars here

+
+ +

<%= user.username %>

+
<% if (user.username === currentUser.username) { %>