Просмотр исходного кода

Merge pull request #4220 from weseek/imprv/middleware-for-forgot-password

Imprv/middleware for forgot password
cao 4 лет назад
Родитель
Сommit
47dd82c11c

+ 1 - 1
packages/app/src/components/PasswordResetRequestForm.jsx

@@ -27,7 +27,7 @@ const PasswordResetRequestForm = (props) => {
       toastSuccess(t('forgot_password.success_to_send_email'));
     }
     catch (err) {
-      toastError('err', err);
+      toastError(err);
     }
   };
 

+ 0 - 24
packages/app/src/server/middlewares/inject-reset-order-by-token-middleware.js

@@ -1,24 +0,0 @@
-const createError = require('http-errors');
-
-module.exports = (crowi, app) => {
-  const PasswordResetOrder = crowi.model('PasswordResetOrder');
-
-  return async(req, res, next) => {
-    const token = req.params.token || req.body.token;
-
-    if (token == null) {
-      req.error = { key: 'token-not-found', message: 'Token not found' };
-    }
-
-    const passwordResetOrder = await PasswordResetOrder.findOne({ token });
-
-    // check if the token is valid
-    if (passwordResetOrder == null || passwordResetOrder.isExpired() || passwordResetOrder.isRevoked) {
-      req.error = { key: 'password-reset-order-is-not-appropriate', message: 'passwordResetOrder is null or expired or revoked' };
-    }
-
-    req.passwordResetOrder = passwordResetOrder;
-
-    return next();
-  };
-};

+ 27 - 0
packages/app/src/server/middlewares/inject-reset-order-by-token-middleware.ts

@@ -0,0 +1,27 @@
+import { NextFunction, Request, Response } from 'express';
+import createError from 'http-errors';
+
+import PasswordResetOrder, { IPasswordResetOrder } from '../models/password-reset-order';
+
+export type ReqWithPasswordResetOrder = Request & {
+  passwordResetOrder: IPasswordResetOrder,
+};
+
+export default async(req: ReqWithPasswordResetOrder, res: Response, next: NextFunction): Promise<void> => {
+  const token = req.params.token || req.body.token;
+
+  if (token == null) {
+    return next(createError(400, 'Token not found', { code: 'token-not-found' }));
+  }
+
+  const passwordResetOrder = await PasswordResetOrder.findOne({ token });
+
+  // check if the token is valid
+  if (passwordResetOrder == null || passwordResetOrder.isExpired() || passwordResetOrder.isRevoked) {
+    return next(createError(400, 'passwordResetOrder is null or expired or revoked', { code: 'password-reset-order-is-not-appropriate' }));
+  }
+
+  req.passwordResetOrder = passwordResetOrder;
+
+  return next();
+};

+ 0 - 1
packages/app/src/server/models/index.js

@@ -17,5 +17,4 @@ module.exports = {
   GlobalNotificationSlackSetting: require('./GlobalNotificationSetting/GlobalNotificationSlackSetting'),
   ShareLink: require('./share-link'),
   SlackAppIntegration: require('./slack-app-integration'),
-  PasswordResetOrder: require('./password-reset-order'),
 };

+ 0 - 57
packages/app/src/server/models/password-reset-order.js

@@ -1,57 +0,0 @@
-const mongoose = require('mongoose');
-const uniqueValidator = require('mongoose-unique-validator');
-const crypto = require('crypto');
-
-const ObjectId = mongoose.Schema.Types.ObjectId;
-
-const schema = new mongoose.Schema({
-  token: { type: String, required: true, unique: true },
-  email: { type: String, required: true },
-  relatedUser: { type: ObjectId, ref: 'User' },
-  isRevoked: { type: Boolean, default: false, required: true },
-  createdAt: { type: Date, default: Date.now, required: true },
-  expiredAt: { type: Date, default: Date.now() + 600000, required: true },
-});
-schema.plugin(uniqueValidator);
-
-class PasswordResetOrder {
-
-  static generateOneTimeToken() {
-    const buf = crypto.randomBytes(256);
-    const token = buf.toString('hex');
-
-    return token;
-  }
-
-  static async createPasswordResetOrder(email) {
-    let token;
-    let duplicateToken;
-
-    do {
-      token = this.generateOneTimeToken();
-      // eslint-disable-next-line no-await-in-loop
-      duplicateToken = await this.findOne({ token });
-    } while (duplicateToken != null);
-
-    const passwordResetOrderData = await this.create({ token, email });
-
-    return passwordResetOrderData;
-  }
-
-  isExpired() {
-    return this.expiredAt.getTime() < Date.now();
-  }
-
-  async revokeOneTimeToken() {
-    this.isRevoked = true;
-    return this.save();
-  }
-
-}
-
-module.exports = function(crowi) {
-  PasswordResetOrder.crowi = crowi;
-  schema.loadClass(PasswordResetOrder);
-  const model = mongoose.model('PasswordResetOrder', schema);
-  return model;
-};

+ 72 - 0
packages/app/src/server/models/password-reset-order.ts

@@ -0,0 +1,72 @@
+import mongoose, {
+  Schema, Model, Document,
+} from 'mongoose';
+
+import uniqueValidator from 'mongoose-unique-validator';
+import crypto from 'crypto';
+import { getOrCreateModel } from '../util/mongoose-utils';
+
+const ObjectId = mongoose.Schema.Types.ObjectId;
+
+export interface IPasswordResetOrder {
+  token: string,
+  email: string,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  relatedUser: any,
+  isRevoked: boolean,
+  createdAt: Date,
+  expiredAt: Date,
+}
+
+export interface PasswordResetOrderDocument extends IPasswordResetOrder, Document {
+  isExpired(): Promise<boolean>
+  revokeOneTimeToken(): Promise<void>
+}
+
+export interface PasswordResetOrderModel extends Model<PasswordResetOrderDocument> {
+  generateOneTimeToken(): string
+  createPasswordResetOrder(email: string): PasswordResetOrderDocument
+}
+
+const schema = new Schema<PasswordResetOrderDocument, PasswordResetOrderModel>({
+  token: { type: String, required: true, unique: true },
+  email: { type: String, required: true },
+  relatedUser: { type: ObjectId, ref: 'User' },
+  isRevoked: { type: Boolean, default: false, required: true },
+  createdAt: { type: Date, default: Date.now, required: true },
+  expiredAt: { type: Date, default: Date.now() + 600000, required: true },
+});
+schema.plugin(uniqueValidator);
+
+schema.statics.generateOneTimeToken = function() {
+  const buf = crypto.randomBytes(256);
+  const token = buf.toString('hex');
+
+  return token;
+};
+
+schema.statics.createPasswordResetOrder = async function(email) {
+  let token;
+  let duplicateToken;
+
+  do {
+    token = this.generateOneTimeToken();
+    // eslint-disable-next-line no-await-in-loop
+    duplicateToken = await this.findOne({ token });
+  } while (duplicateToken != null);
+
+  const passwordResetOrderData = await this.create({ token, email });
+
+  return passwordResetOrderData;
+};
+
+schema.methods.isExpired = function() {
+  return this.expiredAt.getTime() < Date.now();
+};
+
+schema.methods.revokeOneTimeToken = async function() {
+  this.isRevoked = true;
+  return this.save();
+};
+
+export default getOrCreateModel<PasswordResetOrderDocument, PasswordResetOrderModel>('PasswordResetOrder', schema);

+ 15 - 10
packages/app/src/server/routes/apiv3/forgot-password.js

@@ -1,4 +1,8 @@
 import rateLimit from 'express-rate-limit';
+
+import PasswordResetOrder from '~/server/models/password-reset-order';
+import ErrorV3 from '~/server/models/vo/error-apiv3';
+import injectResetOrderByTokenMiddleware from '~/server/middlewares/inject-reset-order-by-token-middleware';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:routes:apiv3:forgotPassword'); // eslint-disable-line no-unused-vars
@@ -11,12 +15,10 @@ const router = express.Router();
 
 module.exports = (crowi) => {
   const { appService, mailService, configManager } = crowi;
-  const PasswordResetOrder = crowi.model('PasswordResetOrder');
   const User = crowi.model('User');
   const path = require('path');
   const csrf = require('../../middlewares/csrf')(crowi);
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
-  const injectResetOrderByTokenMiddleware = require('../../middlewares/inject-reset-order-by-token-middleware')(crowi);
 
   const validator = {
     password: [
@@ -73,18 +75,13 @@ module.exports = (crowi) => {
       return res.apiv3();
     }
     catch (err) {
-      const msg = 'Error occurred during password reset request procedure';
+      const msg = 'Error occurred during password reset request procedure.';
       logger.error(err);
-      return res.apiv3Err(msg);
+      return res.apiv3Err(`${msg} Cause: ${err}`);
     }
   });
 
-  router.put('/', apiLimiter, csrf, injectResetOrderByTokenMiddleware, validator.password, apiV3FormValidator, async(req, res) => {
-
-    if (req.error != null) {
-      return res.apiv3Err(req.error.message);
-    }
-
+  router.put('/', injectResetOrderByTokenMiddleware, async(req, res) => {
     const { passwordResetOrder } = req;
     const { email } = passwordResetOrder;
     const grobalLang = configManager.getConfig('crowi', 'app:globalLang');
@@ -111,5 +108,13 @@ module.exports = (crowi) => {
     }
   });
 
+  // middleware to handle error
+  router.use((error, req, res, next) => {
+    if (error != null) {
+      return res.apiv3Err(new ErrorV3(error.message, error.code));
+    }
+    next();
+  });
+
   return router;
 };

+ 0 - 21
packages/app/src/server/routes/forgot-password.js

@@ -1,21 +0,0 @@
-module.exports = function(crowi, app) {
-  const actions = {};
-  const api = {};
-  actions.api = api;
-
-  actions.forgotPassword = async function(req, res) {
-    return res.render('forgot-password');
-  };
-
-  actions.resetPassword = async function(req, res) {
-    const { error, passwordResetOrder } = req;
-
-    if (error != null) {
-      return res.render('forgot-password/error', { key: error.key });
-    }
-
-    return res.render('reset-password', { email: passwordResetOrder.email });
-  };
-
-  return actions;
-};

+ 21 - 0
packages/app/src/server/routes/forgot-password.ts

@@ -0,0 +1,21 @@
+import {
+  NextFunction, Request, RequestHandler, Response,
+} from 'express';
+import { ReqWithPasswordResetOrder } from '../middlewares/inject-reset-order-by-token-middleware';
+
+export const forgotPassword = (req: Request, res: Response): void => {
+  return res.render('forgot-password');
+};
+
+export const resetPassword = (req: ReqWithPasswordResetOrder, res: Response): void => {
+  const { passwordResetOrder } = req;
+  return res.render('reset-password', { email: passwordResetOrder.email });
+};
+
+// middleware to handle error
+export const handleHttpErrosMiddleware = (error: Error & { code: string }, req: Request, res: Response, next: NextFunction): Promise<RequestHandler> | void => {
+  if (error != null) {
+    return res.render('forgot-password/error', { key: error.code });
+  }
+  next();
+};

+ 10 - 4
packages/app/src/server/routes/index.js

@@ -1,3 +1,9 @@
+import express from 'express';
+
+import injectResetOrderByTokenMiddleware from '../middlewares/inject-reset-order-by-token-middleware';
+
+import * as forgotPassword from './forgot-password';
+
 const multer = require('multer');
 const autoReap = require('multer-autoreap');
 const rateLimit = require('express-rate-limit');
@@ -21,7 +27,6 @@ module.exports = function(crowi, app) {
   const adminRequired = require('../middlewares/admin-required')(crowi);
   const certifySharedFile = require('../middlewares/certify-shared-file')(crowi);
   const csrf = require('../middlewares/csrf')(crowi);
-  const injectResetOrderByTokenMiddleware = require('../middlewares/inject-reset-order-by-token-middleware')(crowi);
 
   const uploads = multer({ dest: `${crowi.tmpDir}uploads` });
   const form = require('../form');
@@ -37,7 +42,6 @@ module.exports = function(crowi, app) {
   const tag = require('./tag')(crowi, app);
   const search = require('./search')(crowi, app);
   const hackmd = require('./hackmd')(crowi, app);
-  const forgotPassword = require('./forgot-password')(crowi, app);
 
   const isInstalled = crowi.configManager.getConfig('crowi', 'app:installed');
 
@@ -185,8 +189,10 @@ module.exports = function(crowi, app) {
   app.post('/_api/hackmd.discard'        , accessTokenParser , loginRequiredStrictly , csrf, hackmd.validateForApi, hackmd.discard);
   app.post('/_api/hackmd.saveOnHackmd'   , accessTokenParser , loginRequiredStrictly , csrf, hackmd.validateForApi, hackmd.saveOnHackmd);
 
-  app.get('/forgot-password', forgotPassword.forgotPassword);
-  app.get('/forgot-password/:token'      ,apiLimiter, injectResetOrderByTokenMiddleware, forgotPassword.resetPassword);
+  app.use('/forgot-password', express.Router()
+    .get('/', forgotPassword.forgotPassword)
+    .get('/:token', apiLimiter, injectResetOrderByTokenMiddleware, forgotPassword.resetPassword)
+    .use(forgotPassword.handleHttpErrosMiddleware));
 
   app.get('/share/:linkId', page.showSharedPage);