Prechádzať zdrojové kódy

add set new password form

Pouria Ezzati 1 rok pred
rodič
commit
4379e6aea5

+ 22 - 21
server/handlers/auth.handler.js

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

+ 6 - 0
server/handlers/locals.handler.js

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

+ 31 - 3
server/handlers/renders.handler.js

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

+ 16 - 0
server/handlers/validators.handler.js

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

+ 12 - 2
server/routes/auth.routes.js

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

+ 1 - 2
server/routes/renders.routes.js

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

+ 42 - 0
server/views/partials/reset_password/new_password_form.hbs

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

+ 5 - 0
server/views/partials/reset_password/new_password_success.hbs

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

+ 1 - 0
server/views/partials/reset_password/form.hbs → server/views/partials/reset_password/request_form.hbs

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

+ 1 - 1
server/views/reset_password.hbs

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

+ 0 - 15
server/views/reset_password_result.hbs

@@ -1,15 +0,0 @@
-{{> header}}
-<section id="reset-password-token" class="section-container verify-page">
-  {{#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>
-  {{else}}
-    <h2>
-      {{> icons/x}}
-      Password token is invalid. Please try again.
-    </h2>
-    <a href="/reset-password" title="Reset password">Reset password →</a>
-  {{/if}}
-</section>
-{{> footer}}

+ 20 - 0
server/views/reset_password_set_new_password.hbs

@@ -0,0 +1,20 @@
+{{> header}}
+<section 
+  id="new-password"
+  class="section-container {{#unless token_verified}}verify-page{{/unless}}"
+>
+  {{#if token_verified}}
+    <h2>
+      Reset password.
+    </h2>
+    <p>Set your new password.</p>
+    {{> reset_password/new_password_form}}
+  {{else}}
+    <h2>
+      {{> icons/x}}
+      Password token is invalid. Please try again.
+    </h2>
+    <a href="/reset-password" title="Reset password">Reset password →</a>
+  {{/if}}
+</section>
+{{> footer}}

+ 44 - 4
static/css/styles.css

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