diff --git a/backend/migrations/20230130052902-passwordReset.js b/backend/migrations/20230130052902-passwordReset.js new file mode 100644 index 0000000..a4ad9b3 --- /dev/null +++ b/backend/migrations/20230130052902-passwordReset.js @@ -0,0 +1,24 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.addColumn("Users", "passwordResetToken", { + type: Sequelize.STRING, + allowNull: true + }) + await queryInterface.addColumn("Users", "passwordResetExpiry", { + type: Sequelize.DATE, + allowNull: true + }) + }, + + async down (queryInterface, Sequelize) { + /** + * Add reverting commands here. + * + * Example: + * await queryInterface.dropTable('users'); + */ + } +}; diff --git a/backend/models/users.js b/backend/models/users.js index 9246ceb..bfbd832 100644 --- a/backend/models/users.js +++ b/backend/models/users.js @@ -157,6 +157,14 @@ module.exports = (sequelize, DataTypes) => { emailToken: { type: DataTypes.STRING, allowNull: true + }, + passwordResetToken: { + type: DataTypes.STRING, + allowNull: true + }, + passwordResetExpiry: { + type: DataTypes.DATE, + allowNull: true } }, { diff --git a/backend/routes/user.js b/backend/routes/user.js index c4056f3..40c9d32 100644 --- a/backend/routes/user.js +++ b/backend/routes/user.js @@ -589,4 +589,103 @@ router.put("/settings/:type", auth, async (req, res, next) => { } }) +router.post("/reset/send", mailLimiter, async (req, res, next) => { + try { + const user = await User.findOne({ + where: { + email: req.body.email + } + }) + if (user) { + const token = cryptoRandomString({ length: 64 }) + await user.update({ + passwordResetToken: token, + passwordResetExpiry: Date.now() + 86400000 + }) + const mailGenerator = new Mailgen({ + theme: "default", + product: { + name: req.app.locals.config.siteName, + link: req.app.locals.config.corsHostname + } + }) + const email = { + body: { + name: user.username, + intro: `${req.app.locals.config.siteName} Password Reset`, + action: { + instructions: `You are receiving this email because you registered on ${req.app.locals.config.siteName}, please use the link below to reset your account's password.`, + button: { + color: "#1A97FF", + text: "Password Reset", + link: req.app.locals.config.corsHostname + "/reset/" + token + } + }, + outro: "If you did not request a password reset, ignore this email." + } + } + const emailBody = mailGenerator.generate(email) + + const emailText = mailGenerator.generatePlaintext(email) + const transporter = nodemailer.createTransport({ + host: req.app.locals.config.emailSMTPHost, + port: req.app.locals.config.emailSMTPPort, + secure: req.app.locals.config.emailSMTPSecure, + auth: { + user: req.app.locals.config.emailSMTPUser, + pass: req.app.locals.config.emailSMTPPassword + } + }) + let info = await transporter.sendMail({ + from: req.app.locals.config.emailSMTPFrom, + to: user.email, + subject: "Password Reset - " + req.app.locals.config.siteName, + text: emailText, + html: emailBody + }) + res.sendStatus(204) + } else { + throw Errors.customMessage( + "Email not found, please type your email, not your username." + ) + } + } catch (e) { + console.log(e) + next(e) + } +}) + +router.put("/reset", async (req, res, next) => { + try { + if (req.body.password && req.body.token) { + const user = await User.findOne({ + where: { + passwordResetToken: req.body.token, + passwordResetExpiry: { + [Op.gt]: Date.now() + } + } + }) + if (!req.body.token) { + throw Errors.invalidParameter("Token", "Invalid or expired token") + } + if (user) { + await user.update({ + password: await argon2.hash(req.body.password), + passwordResetToken: null, + passwordResetExpiry: null + }) + res.sendStatus(204) + } else { + throw Errors.invalidParameter("Token", "Invalid or expired token") + } + } else { + throw Errors.invalidParameter("Password or Token") + } + } catch (e) { + console.log(e) + next(e) + } +}) + module.exports = router diff --git a/bot/.env b/bot/.env new file mode 100644 index 0000000..ea81d38 --- /dev/null +++ b/bot/.env @@ -0,0 +1,3 @@ +BC_TOKEN=BETTERCOMPASS-543c09baab5e66e5091574a699c5efb2826f942e3b2d24dbb3a585557a9018751e1b5d693d5a9355566eb249d0c03a3086f0198e6f68ed589f7b1929f69ef88d +TOKEN=COLUBRINA-755948eea43906bd28b8eed81b6966586f18564438db21bd73f6ba8966e0631aac096dd1ef16d30b95c26ba580765cc83882e625fce8d473e3cca4abd326a9b4 +HOSTNAME=https://colubrina.troplo.com \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 33e2582..2632d87 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "colubrina", - "version": "1.0.36", + "version": "1.0.37", "description": "Simple instant communication.", "private": true, "author": "Troplo ", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d0e2ffb..44927db 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -822,7 +822,9 @@ export default { }) .catch(() => { this.$store.dispatch("logout") - this.$router.push("/login") + if (!window.location.pathname.includes("/reset")) { + this.$router.push("/login") + } }) this.getThemes() this.communicationsIdleCheck() diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index d70ab78..c034919 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -170,6 +170,14 @@ const routes = [ /* webpackChunkName: "emailConfirm" */ "../views/Email/EmailConfirm.vue" ) }, + { + path: "/reset/:code", + name: "Reset Password", + component: () => + import( + /* webpackChunkName: "resetPassword" */ "../views/PasswordReset.vue" + ) + }, { path: "*", name: "Not Found", diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index 184af21..89451af 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -71,6 +71,12 @@ label="Password" type="password" > +

+ Reset your Password +

{ + this.loading = false + this.$toast.success("Password reset sent, check your email!") + }).catch((e) => { + this.loading = false + AjaxErrorHandler(this.$store)(e) + }) + }, isElectron() { return process.env.IS_ELECTRON }, diff --git a/frontend/src/views/PasswordReset.vue b/frontend/src/views/PasswordReset.vue new file mode 100644 index 0000000..3ea08b3 --- /dev/null +++ b/frontend/src/views/PasswordReset.vue @@ -0,0 +1,80 @@ + + + + +