This commit is contained in:
Troplo 2023-01-30 17:16:38 +11:00
parent 6ff737b651
commit 7a8e4cdb7d
9 changed files with 244 additions and 2 deletions

View file

@ -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');
*/
}
};

View file

@ -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
}
},
{

View file

@ -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

3
bot/.env Normal file
View file

@ -0,0 +1,3 @@
BC_TOKEN=BETTERCOMPASS-543c09baab5e66e5091574a699c5efb2826f942e3b2d24dbb3a585557a9018751e1b5d693d5a9355566eb249d0c03a3086f0198e6f68ed589f7b1929f69ef88d
TOKEN=COLUBRINA-755948eea43906bd28b8eed81b6966586f18564438db21bd73f6ba8966e0631aac096dd1ef16d30b95c26ba580765cc83882e625fce8d473e3cca4abd326a9b4
HOSTNAME=https://colubrina.troplo.com

View file

@ -1,6 +1,6 @@
{
"name": "colubrina",
"version": "1.0.36",
"version": "1.0.37",
"description": "Simple instant communication.",
"private": true,
"author": "Troplo <troplo@troplo.com>",

View file

@ -822,7 +822,9 @@ export default {
})
.catch(() => {
this.$store.dispatch("logout")
if (!window.location.pathname.includes("/reset")) {
this.$router.push("/login")
}
})
this.getThemes()
this.communicationsIdleCheck()

View file

@ -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",

View file

@ -71,6 +71,12 @@
label="Password"
type="password"
></v-text-field>
<p
style="float: right; color: #2196f3; cursor: pointer"
@click="doPasswordReset()"
>
Reset your Password
</p>
<v-switch
inset
label="Remember Me"
@ -128,6 +134,18 @@ export default {
}
},
methods: {
doPasswordReset() {
this.loading = true
this.axios.post("/api/v1/user/reset/send", {
email: this.username
}).then(() => {
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
},

View file

@ -0,0 +1,80 @@
<template>
<div id="password-reset">
<v-container>
<v-card class="rounded-xl">
<v-card-title class="rounded-xl">
<h1 class="display-1">Reset your Password</h1>
</v-card-title>
<v-card-text>
<v-form>
<v-text-field
@keyup.enter="doPasswordReset()"
class="rounded-xl"
v-model="password"
label="Password"
type="password"
></v-text-field>
<v-text-field
@keyup.enter="doPasswordReset()"
class="rounded-xl"
v-model="confirmPassword"
label="Confirm Password"
type="password"
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
class="rounded-xl"
:loading="loading"
color="primary"
text
@click="doPasswordReset()"
>
Reset Password
</v-btn>
</v-card-actions>
</v-card>
</v-container>
</div>
</template>
<script>
import AjaxErrorHandler from "@/lib/errorHandler"
export default {
name: "PasswordReset",
data() {
return {
password: "",
confirmPassword: "",
loading: false
}
},
methods: {
doPasswordReset() {
if (this.password !== this.confirmPassword) {
this.$toast.error("Passwords do not match")
return
}
this.loading = true
this.axios
.put("/api/v1/user/reset", {
password: this.password,
token: this.$route.params.code
})
.then(() => {
this.$router.push("/login")
this.$toast.success("Password reset successfully")
})
.catch((e) => {
this.loading = false
AjaxErrorHandler(this.$store)(e)
})
}
}
}
</script>
<style scoped></style>