UNPKG

@rytass/member-base-nestjs-module

Version:

Rytass Member System NestJS Base Module

137 lines (134 loc) 6.37 kB
import { Injectable, Inject } from '@nestjs/common'; import { verify } from 'argon2'; import { PASSWORD_SHOULD_INCLUDE_UPPERCASE, PASSWORD_SHOULD_INCLUDE_LOWERCASE, PASSWORD_SHOULD_INCLUDE_DIGIT, PASSWORD_SHOULD_INCLUDE_SPECIAL_CHARACTER, PASSWORD_MIN_LENGTH, PASSWORD_POLICY_REGEXP, PASSWORD_HISTORY_LIMIT, PASSWORD_AGE_LIMIT_IN_DAYS } from '../typings/member-base.tokens.js'; import { MemberPasswordHistoryRepo } from '../models/member-password-history.entity.js'; import { Repository } from 'typeorm'; import { DateTime } from 'luxon'; import { PasswordInHistoryError } from '../constants/errors/base.error.js'; function _ts_decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } function _ts_metadata(k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); } function _ts_param(paramIndex, decorator) { return function(target, key) { decorator(target, key, paramIndex); }; } // Use runtime require to avoid depending on @types/generate-password at consumers const { generate } = require('generate-password'); class PasswordValidatorService { passwordShouldIncludeUppercase; passwordShouldIncludeLowercase; passwordShouldIncludeDigit; passwordShouldIncludeSpecialCharacter; passwordMinLength; passwordPolicyRegExp; passwordHistoryLimit; memberPasswordHistoryRepo; passwordAgeLimitInDays; constructor(passwordShouldIncludeUppercase, passwordShouldIncludeLowercase, passwordShouldIncludeDigit, passwordShouldIncludeSpecialCharacter, passwordMinLength, passwordPolicyRegExp, passwordHistoryLimit, memberPasswordHistoryRepo, passwordAgeLimitInDays){ this.passwordShouldIncludeUppercase = passwordShouldIncludeUppercase; this.passwordShouldIncludeLowercase = passwordShouldIncludeLowercase; this.passwordShouldIncludeDigit = passwordShouldIncludeDigit; this.passwordShouldIncludeSpecialCharacter = passwordShouldIncludeSpecialCharacter; this.passwordMinLength = passwordMinLength; this.passwordPolicyRegExp = passwordPolicyRegExp; this.passwordHistoryLimit = passwordHistoryLimit; this.memberPasswordHistoryRepo = memberPasswordHistoryRepo; this.passwordAgeLimitInDays = passwordAgeLimitInDays; } generateValidPassword() { if (!this.passwordShouldIncludeDigit && !this.passwordShouldIncludeLowercase && !this.passwordShouldIncludeSpecialCharacter && !this.passwordShouldIncludeUppercase) { // No specific policy, just generate a simple password return generate({ length: this.passwordMinLength, lowercase: true }); } return generate({ length: this.passwordMinLength, numbers: this.passwordShouldIncludeDigit, uppercase: this.passwordShouldIncludeUppercase, lowercase: this.passwordShouldIncludeLowercase, symbols: this.passwordShouldIncludeSpecialCharacter }); } shouldUpdatePassword(member) { if (!this.passwordAgeLimitInDays) return false; const validBefore = DateTime.fromJSDate(member.passwordChangedAt).plus({ days: this.passwordAgeLimitInDays }).endOf('day').toMillis(); return validBefore < DateTime.now().toMillis(); } async validatePassword(password, memberId) { const trimmed = password.trim(); if (this.passwordPolicyRegExp) { return this.passwordPolicyRegExp.test(trimmed); } if (trimmed.length < this.passwordMinLength) { return false; } if (this.passwordShouldIncludeUppercase && !/[A-Z]/.test(trimmed)) { return false; } if (this.passwordShouldIncludeLowercase && !/[a-z]/.test(trimmed)) { return false; } if (this.passwordShouldIncludeDigit && !/[0-9]/.test(trimmed)) { return false; } if (this.passwordShouldIncludeSpecialCharacter && !/[!><.,?@#$%^&*;:'"|\\/~`()-_+={}[\]]/.test(trimmed)) { return false; } if (this.passwordHistoryLimit) { const qb = this.memberPasswordHistoryRepo.createQueryBuilder('histories'); qb.andWhere('histories.memberId = :memberId', { memberId }); qb.addOrderBy('histories.createdAt', 'DESC'); qb.take(this.passwordHistoryLimit); const histories = await qb.getMany(); try { await histories.map((history)=>async ()=>{ const isVerify = await verify(history.password, password); if (isVerify) { throw new PasswordInHistoryError(); } }).reduce((prev, next)=>prev.then(next), Promise.resolve()); } catch (_ex) { return false; } } return true; } } PasswordValidatorService = _ts_decorate([ Injectable(), _ts_param(0, Inject(PASSWORD_SHOULD_INCLUDE_UPPERCASE)), _ts_param(1, Inject(PASSWORD_SHOULD_INCLUDE_LOWERCASE)), _ts_param(2, Inject(PASSWORD_SHOULD_INCLUDE_DIGIT)), _ts_param(3, Inject(PASSWORD_SHOULD_INCLUDE_SPECIAL_CHARACTER)), _ts_param(4, Inject(PASSWORD_MIN_LENGTH)), _ts_param(5, Inject(PASSWORD_POLICY_REGEXP)), _ts_param(6, Inject(PASSWORD_HISTORY_LIMIT)), _ts_param(7, Inject(MemberPasswordHistoryRepo)), _ts_param(8, Inject(PASSWORD_AGE_LIMIT_IN_DAYS)), _ts_metadata("design:type", Function), _ts_metadata("design:paramtypes", [ Boolean, Boolean, Boolean, Boolean, Number, Object, Object, typeof Repository === "undefined" ? Object : Repository, Object ]) ], PasswordValidatorService); export { PasswordValidatorService };