mirror of
https://github.com/Troplo/Colubrina.git
synced 2024-11-28 05:58:59 +11:00
1.0.37
This commit is contained in:
parent
6ff737b651
commit
7a8e4cdb7d
9 changed files with 244 additions and 2 deletions
24
backend/migrations/20230130052902-passwordReset.js
Normal file
24
backend/migrations/20230130052902-passwordReset.js
Normal 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');
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
};
|
|
@ -157,6 +157,14 @@ module.exports = (sequelize, DataTypes) => {
|
||||||
emailToken: {
|
emailToken: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
|
},
|
||||||
|
passwordResetToken: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
passwordResetExpiry: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
module.exports = router
|
||||||
|
|
3
bot/.env
Normal file
3
bot/.env
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
BC_TOKEN=BETTERCOMPASS-543c09baab5e66e5091574a699c5efb2826f942e3b2d24dbb3a585557a9018751e1b5d693d5a9355566eb249d0c03a3086f0198e6f68ed589f7b1929f69ef88d
|
||||||
|
TOKEN=COLUBRINA-755948eea43906bd28b8eed81b6966586f18564438db21bd73f6ba8966e0631aac096dd1ef16d30b95c26ba580765cc83882e625fce8d473e3cca4abd326a9b4
|
||||||
|
HOSTNAME=https://colubrina.troplo.com
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "colubrina",
|
"name": "colubrina",
|
||||||
"version": "1.0.36",
|
"version": "1.0.37",
|
||||||
"description": "Simple instant communication.",
|
"description": "Simple instant communication.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": "Troplo <troplo@troplo.com>",
|
"author": "Troplo <troplo@troplo.com>",
|
||||||
|
|
|
@ -822,7 +822,9 @@ export default {
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.$store.dispatch("logout")
|
this.$store.dispatch("logout")
|
||||||
this.$router.push("/login")
|
if (!window.location.pathname.includes("/reset")) {
|
||||||
|
this.$router.push("/login")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
this.getThemes()
|
this.getThemes()
|
||||||
this.communicationsIdleCheck()
|
this.communicationsIdleCheck()
|
||||||
|
|
|
@ -170,6 +170,14 @@ const routes = [
|
||||||
/* webpackChunkName: "emailConfirm" */ "../views/Email/EmailConfirm.vue"
|
/* webpackChunkName: "emailConfirm" */ "../views/Email/EmailConfirm.vue"
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/reset/:code",
|
||||||
|
name: "Reset Password",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "resetPassword" */ "../views/PasswordReset.vue"
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "*",
|
||||||
name: "Not Found",
|
name: "Not Found",
|
||||||
|
|
|
@ -71,6 +71,12 @@
|
||||||
label="Password"
|
label="Password"
|
||||||
type="password"
|
type="password"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
<p
|
||||||
|
style="float: right; color: #2196f3; cursor: pointer"
|
||||||
|
@click="doPasswordReset()"
|
||||||
|
>
|
||||||
|
Reset your Password
|
||||||
|
</p>
|
||||||
<v-switch
|
<v-switch
|
||||||
inset
|
inset
|
||||||
label="Remember Me"
|
label="Remember Me"
|
||||||
|
@ -128,6 +134,18 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
isElectron() {
|
||||||
return process.env.IS_ELECTRON
|
return process.env.IS_ELECTRON
|
||||||
},
|
},
|
||||||
|
|
80
frontend/src/views/PasswordReset.vue
Normal file
80
frontend/src/views/PasswordReset.vue
Normal 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>
|
Loading…
Reference in a new issue