add set new password form

This commit is contained in:
Pouria Ezzati 2024-12-31 16:24:37 +03:30
parent 2c83f8e2d8
commit 4379e6aea5
12 changed files with 189 additions and 36 deletions

View File

@ -223,7 +223,7 @@ async function generateApiKey(req, res) {
return res.status(201).send({ apikey });
}
async function resetPasswordRequest(req, res) {
async function resetPassword(req, res) {
const user = await query.user.update(
{ email: req.body.email },
{
@ -239,7 +239,7 @@ async function resetPasswordRequest(req, res) {
}
if (req.isHTML) {
res.render("partials/reset_password/form", {
res.render("partials/reset_password/request_form", {
message: "If the email address exists, a reset password email will be sent to it."
});
return;
@ -250,28 +250,29 @@ async function resetPasswordRequest(req, res) {
});
}
async function resetPassword(req, res, next) {
const resetPasswordToken = req.params.resetPasswordToken;
async function newPassword(req, res) {
const { new_password, reset_password_token } = req.body;
if (resetPasswordToken) {
const user = await query.user.update(
{
reset_password_token: resetPasswordToken,
reset_password_expires: [">", utils.dateToUTC(new Date())]
},
{ reset_password_expires: null, reset_password_token: null }
);
if (user) {
const token = utils.signToken(user);
utils.deleteCurrentToken(res);
utils.setToken(res, token);
res.locals.token_verified = true;
req.cookies.token = token;
const salt = await bcrypt.genSalt(12);
const password = await bcrypt.hash(req.body.new_password, salt);
const user = await query.user.update(
{
reset_password_token,
reset_password_expires: [">", utils.dateToUTC(new Date())]
},
{
reset_password_expires: null,
reset_password_token: null,
password,
}
);
if (!user) {
throw new CustomError("Could not set the password. Please try again later.");
}
next();
res.render("partials/reset_password/new_password_success");
}
async function changeEmailRequest(req, res) {
@ -386,8 +387,8 @@ module.exports = {
jwtPage,
local,
login,
newPassword,
resetPassword,
resetPasswordRequest,
signup,
verify,
}

View File

@ -37,6 +37,11 @@ async function user(req, res, next) {
next();
}
function newPassword(req, res, next) {
res.locals.reset_password_token = req.body.reset_password_token;
next();
}
function createLink(req, res, next) {
res.locals.show_advanced = !!req.body.show_advanced;
next();
@ -73,6 +78,7 @@ module.exports = {
createLink,
editLink,
isHTML,
newPassword,
noLayout,
protected,
user,

View File

@ -2,6 +2,12 @@ const query = require("../queries");
const utils = require("../utils");
const env = require("../env");
/**
*
* PAGES
*
**/
async function homepage(req, res) {
// redirect to custom domain homepage if it is set by user
const host = utils.removeWww(req.headers.host);
@ -100,9 +106,25 @@ async function resetPassword(req, res) {
});
}
async function resetPasswordResult(req, res) {
res.render("reset_password_result", {
async function resetPasswordSetNewPassword(req, res) {
const reset_password_token = req.params.resetPasswordToken;
if (reset_password_token) {
const user = await query.user.find(
{
reset_password_token,
reset_password_expires: [">", utils.dateToUTC(new Date())]
}
);
if (user) {
res.locals.token_verified = true;
}
}
res.render("reset_password_set_new_password", {
title: "Reset password",
...(res.locals.token_verified && { reset_password_token }),
});
}
@ -124,6 +146,12 @@ async function terms(req, res) {
});
}
/**
*
* PARTIALS
*
**/
async function confirmLinkDelete(req, res) {
const link = await query.link.find({
uuid: req.query.id,
@ -311,7 +339,7 @@ module.exports = {
notFound,
report,
resetPassword,
resetPasswordResult,
resetPasswordSetNewPassword,
settings,
stats,
terms,

View File

@ -469,6 +469,21 @@ const resetPassword = [
.isEmail()
];
const newPassword = [
body("reset_password_token", "Reset password token is invalid.")
.exists({ checkFalsy: true, checkNull: true })
.isLength({ min: 36, max: 36 }),
body("new_password", "Password is not valid.")
.exists({ checkFalsy: true, checkNull: true })
.isLength({ min: 8, max: 64 })
.withMessage("Password length must be between 8 and 64."),
body("repeat_password", "Password is not valid.")
.custom((repeat_password, { req }) => {
return repeat_password === req.body.new_password;
})
.withMessage("Passwords don't match."),
];
const deleteUser = [
body("password", "Password is not valid.")
.exists({ checkFalsy: true, checkNull: true })
@ -607,6 +622,7 @@ module.exports = {
getStats,
login,
malware,
newPassword,
redirectProtected,
removeDomain,
removeDomainAdmin,

View File

@ -72,12 +72,22 @@ router.post(
router.post(
"/reset-password",
locals.viewTemplate("partials/reset_password/form"),
locals.viewTemplate("partials/reset_password/request_form"),
auth.featureAccess([env.MAIL_ENABLED]),
validators.resetPassword,
asyncHandler(helpers.verify),
helpers.rateLimit({ window: 60, limit: 3 }),
asyncHandler(auth.resetPasswordRequest)
asyncHandler(auth.resetPassword)
);
router.post(
"/new-password",
locals.viewTemplate("partials/reset_password/new_password_form"),
locals.newPassword,
validators.newPassword,
asyncHandler(helpers.verify),
helpers.rateLimit({ window: 60, limit: 5 }),
asyncHandler(auth.newPassword)
);
module.exports = router;

View File

@ -86,10 +86,9 @@ router.get(
router.get(
"/reset-password/:resetPasswordToken",
asyncHandler(auth.resetPassword),
asyncHandler(auth.jwtLoosePage),
asyncHandler(locals.user),
asyncHandler(renders.resetPasswordResult)
asyncHandler(renders.resetPasswordSetNewPassword)
);
router.get(

View File

@ -0,0 +1,42 @@
<form
id="new-password-form"
class="htmx-spinner"
hx-post="/api/auth/new-password"
hx-vals='{"reset_password_token":"{{reset_password_token}}"}'
hx-sync="this:abort"
hx-swap="outerHTML"
>
<label class="{{#if errors.new_password}}error{{/if}}">
New password:
<input
id="new_password"
name="new_password"
type="password"
placeholder="New password..."
hx-preserve="true"
required
/>
{{#if errors.new_password}}<p class="error">{{errors.new_password}}</p>{{/if}}
</label>
<label class="{{#if errors.repeat_password}}error{{/if}}">
Repeat password:
<input
id="repeat_password"
name="repeat_password"
type="password"
placeholder="Repeat password..."
hx-preserve="true"
required
/>
{{#if errors.repeat_password}}<p class="error">{{errors.repeat_password}}</p>{{/if}}
</label>
<button type="submit" class="primary">
<span>{{> icons/spinner}}</span>
Set password
</button>
{{#unless errors}}
{{#if error}}
<p class="error">{{error}}</p>
{{/if}}
{{/unless}}
</form>

View File

@ -0,0 +1,5 @@
<p class="success">
Your password is updated successfully.
You can now log in with your new password.
</p>
<a href="/login" title="Log in">Log in →</a>

View File

@ -1,5 +1,6 @@
<form
id="reset-password-form"
class="htmx-spinner"
hx-post="/api/auth/reset-password"
hx-sync="this:abort"
hx-swap="outerHTML"

View File

@ -7,6 +7,6 @@
If you forgot you password you can use the form below to get a reset
password link.
</p>
{{> reset_password/form}}
{{> reset_password/request_form}}
</section>
{{> footer}}

View File

@ -1,9 +1,14 @@
{{> header}}
<section id="reset-password-token" class="section-container verify-page">
<section
id="new-password"
class="section-container {{#unless token_verified}}verify-page{{/unless}}"
>
{{#if token_verified}}
<h2 hx-get="/settings" hx-trigger="load delay:1s" hx-target="body" hx-push-url="/settings">
Welcome back. Change your password from the settings page. Redirecting...
<h2>
Reset password.
</h2>
<p>Set your new password.</p>
{{> reset_password/new_password_form}}
{{else}}
<h2>
{{> icons/x}}

View File

@ -985,6 +985,10 @@ table .tab a:not(.active):hover {
margin-top: 1rem;
}
.htmx-spinner .spinner { display: none; }
.htmx-spinner.htmx-request button svg { display: none; }
.htmx-spinner.htmx-request .spinner { display: block; }
/* LOGIN & SIGNUP */
form#login-signup {
@ -2192,15 +2196,42 @@ svg.map path.active { stroke: hsl(261, 46%, 50%); stroke-width: 1.5; }
display: flex;
align-items: flex-end;
margin-top: 2rem;
}
#reset-password form label { flex: 0 0 280px; }
#reset-password form label input { width: 100%; }
#reset-password form button { margin: 0 0 0.2rem 1rem; }
#reset-password .spinner { display: none; }
#reset-password .htmx-request svg { display: none; }
#reset-password .htmx-request .spinner { display: block; }
#new-password h2 { margin-bottom: 0.5rem; }
#new-password p { margin-bottom: 1.5rem; }
#new-password-form label { margin-bottom: 1.5rem; }
#new-password-form label input { width: 280px; }
#new-password form {
width: 420px;
max-width: 100%;
flex: 1 1 auto;
display: flex;
padding: 0 16px;
flex-direction: column;
}
#new-password form label { margin-bottom: 2rem; }
#new-password form input {
width: 100%;
height: 72px;
margin-top: 1rem;
padding: 0 3rem;
font-size: 16px;
}
#new-password form button {
height: 56px;
padding: 0 1rem 2px;
margin: 0;
}
/* VERIFY USER */
/* VERIFY CHANGE EMAIL */
@ -2425,6 +2456,15 @@ svg.map path.active { stroke: hsl(261, 46%, 50%); stroke-width: 1.5; }
#reset-password form label { flex-basis: 0; width: 280px; }
#reset-password form button { margin: 0.75rem 0 0.2rem 0; }
#new-password form label { margin-bottom: 1.5rem; }
#new-password form input {
height: 58px;
margin-top: 0.75rem;
padding: 0 2rem;
font-size: 15px;
}
#new-password form button { height: 44px; }
.verify-page h2,
.verify-page h3 { display: flex; flex-direction: column; }