@rytass/member-base-nestjs-module
Version:
Rytass Member System NestJS Base Module
137 lines (134 loc) • 6.37 kB
JavaScript
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 };