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: {
|
||||
type: DataTypes.STRING,
|
||||
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
|
||||
|
|
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",
|
||||
"version": "1.0.36",
|
||||
"version": "1.0.37",
|
||||
"description": "Simple instant communication.",
|
||||
"private": true,
|
||||
"author": "Troplo <troplo@troplo.com>",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
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