UNPKG

@rytass/member-base-nestjs-module

Version:

Rytass Member System NestJS Base Module

1,292 lines (1,268 loc) 68.8 kB
'use strict'; var common = require('@nestjs/common'); var typeorm$1 = require('@nestjs/typeorm'); var typeorm = require('typeorm'); var argon2 = require('argon2'); var jsonwebtoken = require('jsonwebtoken'); var luxon = require('luxon'); var generatePassword = require('generate-password'); var core = require('@nestjs/core'); var node_crypto = require('node:crypto'); var casbin = require('casbin'); var axios = require('axios'); var graphql = require('@nestjs/graphql'); function _ts_decorate$e(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : 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; } const MemberLoginLogRepo = Symbol('MemberLoginLogRepo'); class MemberLoginLogEntity { id; memberId; success; ip; createdAt; member; } _ts_decorate$e([ typeorm.PrimaryGeneratedColumn('uuid') ], MemberLoginLogEntity.prototype, "id", void 0); _ts_decorate$e([ typeorm.Column('uuid'), typeorm.Index() ], MemberLoginLogEntity.prototype, "memberId", void 0); _ts_decorate$e([ typeorm.Column('boolean', { default: true }) ], MemberLoginLogEntity.prototype, "success", void 0); _ts_decorate$e([ typeorm.Column('cidr', { nullable: true }) ], MemberLoginLogEntity.prototype, "ip", void 0); _ts_decorate$e([ typeorm.CreateDateColumn() ], MemberLoginLogEntity.prototype, "createdAt", void 0); _ts_decorate$e([ typeorm.ManyToOne(()=>BaseMemberEntity, (member)=>member.loginLogs), typeorm.JoinColumn({ name: 'memberId', referencedColumnName: 'id' }) ], MemberLoginLogEntity.prototype, "member", void 0); MemberLoginLogEntity = _ts_decorate$e([ typeorm.Entity('member_login_logs') ], MemberLoginLogEntity); function _ts_decorate$d(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : 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; } const MemberOAuthRecordRepo = Symbol('MemberOAuthRecordRepo'); class MemberOAuthRecordEntity { memberId; channel; channelIdentifier; member; } _ts_decorate$d([ typeorm.PrimaryColumn('uuid'), typeorm.Index() ], MemberOAuthRecordEntity.prototype, "memberId", void 0); _ts_decorate$d([ typeorm.PrimaryColumn('varchar') ], MemberOAuthRecordEntity.prototype, "channel", void 0); _ts_decorate$d([ typeorm.Column('varchar') ], MemberOAuthRecordEntity.prototype, "channelIdentifier", void 0); _ts_decorate$d([ typeorm.ManyToOne(()=>BaseMemberEntity, (member)=>member.oauthRecords), typeorm.JoinColumn({ name: 'memberId', referencedColumnName: 'id' }) ], MemberOAuthRecordEntity.prototype, "member", void 0); MemberOAuthRecordEntity = _ts_decorate$d([ typeorm.Entity('member_oauth_records') ], MemberOAuthRecordEntity); function _ts_decorate$c(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : 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; } const BaseMemberRepo = Symbol('BaseMemberRepo'); class BaseMemberEntity { id; account; password; passwordChangedAt; resetPasswordRequestedAt; loginFailedCounter; shouldUpdatePassword; createdAt; updatedAt; deletedAt; loginLogs; passwordHistories; oauthRecords; } _ts_decorate$c([ typeorm.PrimaryGeneratedColumn('uuid') ], BaseMemberEntity.prototype, "id", void 0); _ts_decorate$c([ typeorm.Column('varchar') ], BaseMemberEntity.prototype, "account", void 0); _ts_decorate$c([ typeorm.Column('varchar') ], BaseMemberEntity.prototype, "password", void 0); _ts_decorate$c([ typeorm.Column('timestamptz', { default: 'now()' }) ], BaseMemberEntity.prototype, "passwordChangedAt", void 0); _ts_decorate$c([ typeorm.Column('timestamptz', { nullable: true }) ], BaseMemberEntity.prototype, "resetPasswordRequestedAt", void 0); _ts_decorate$c([ typeorm.Column('int2', { default: 0 }) ], BaseMemberEntity.prototype, "loginFailedCounter", void 0); _ts_decorate$c([ typeorm.Column('boolean', { default: false, comment: 'Member should update password before login' }) ], BaseMemberEntity.prototype, "shouldUpdatePassword", void 0); _ts_decorate$c([ typeorm.CreateDateColumn() ], BaseMemberEntity.prototype, "createdAt", void 0); _ts_decorate$c([ typeorm.UpdateDateColumn() ], BaseMemberEntity.prototype, "updatedAt", void 0); _ts_decorate$c([ typeorm.DeleteDateColumn() ], BaseMemberEntity.prototype, "deletedAt", void 0); _ts_decorate$c([ typeorm.OneToMany(()=>MemberLoginLogEntity, (log)=>log.member) ], BaseMemberEntity.prototype, "loginLogs", void 0); _ts_decorate$c([ typeorm.OneToMany(()=>MemberLoginLogEntity, (log)=>log.member) ], BaseMemberEntity.prototype, "passwordHistories", void 0); _ts_decorate$c([ typeorm.OneToMany(()=>MemberOAuthRecordEntity, (record)=>record.member) ], BaseMemberEntity.prototype, "oauthRecords", void 0); BaseMemberEntity = _ts_decorate$c([ typeorm.Entity('members'), typeorm.TableInheritance({ column: { type: 'varchar', name: 'entityName' } }), typeorm.Index([ 'account' ], { unique: true, where: '"deletedAt" IS NULL' }) ], BaseMemberEntity); function _ts_decorate$b(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : 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; } const MemberPasswordHistoryRepo = Symbol('MemberPasswordHistoryRepo'); class MemberPasswordHistoryEntity { id; memberId; password; createdAt; member; } _ts_decorate$b([ typeorm.PrimaryGeneratedColumn('uuid') ], MemberPasswordHistoryEntity.prototype, "id", void 0); _ts_decorate$b([ typeorm.Column('uuid'), typeorm.Index() ], MemberPasswordHistoryEntity.prototype, "memberId", void 0); _ts_decorate$b([ typeorm.Column('varchar') ], MemberPasswordHistoryEntity.prototype, "password", void 0); _ts_decorate$b([ typeorm.CreateDateColumn() ], MemberPasswordHistoryEntity.prototype, "createdAt", void 0); _ts_decorate$b([ typeorm.ManyToOne(()=>BaseMemberEntity, (member)=>member.passwordHistories), typeorm.JoinColumn({ name: 'memberId', referencedColumnName: 'id' }) ], MemberPasswordHistoryEntity.prototype, "member", void 0); MemberPasswordHistoryEntity = _ts_decorate$b([ typeorm.Entity('member_password_histories') ], MemberPasswordHistoryEntity); function _ts_decorate$a(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; } const models = [ [ BaseMemberRepo, BaseMemberEntity ], [ MemberLoginLogRepo, MemberLoginLogEntity ], [ MemberPasswordHistoryRepo, MemberPasswordHistoryEntity ], [ MemberOAuthRecordRepo, MemberOAuthRecordEntity ] ]; class MemberBaseModelsModule { } MemberBaseModelsModule = _ts_decorate$a([ common.Module({ imports: [ typeorm$1.TypeOrmModule.forFeature(models.map((model)=>model[1])) ], providers: models.map(([symbol, entity])=>({ provide: symbol, useFactory: (dataSource)=>dataSource.getRepository(entity), inject: [ typeorm.DataSource ] })), exports: models.map((model)=>model[0]) }) ], MemberBaseModelsModule); const MEMBER_BASE_MODULE_OPTIONS = Symbol('MEMBER_BASE_MODULE_OPTIONS'); const LOGIN_FAILED_BAN_THRESHOLD = Symbol('LOGIN_FAILED_BAN_THRESHOLD'); const RESET_PASSWORD_TOKEN_EXPIRATION = Symbol('RESET_PASSWORD_TOKEN_EXPIRATION'); const RESET_PASSWORD_TOKEN_SECRET = Symbol('RESET_PASSWORD_TOKEN_SECRET'); const CASBIN_ENFORCER = Symbol('CASBIN_ENFORCER'); const CASBIN_PERMISSION_DECORATOR = Symbol('CASBIN_PERMISSION_DECORATOR'); const CASBIN_PERMISSION_CHECKER = Symbol('CASBIN_PERMISSION_CHECKER'); const ACCESS_TOKEN_SECRET = Symbol('ACCESS_TOKEN_SECRET'); const ACCESS_TOKEN_EXPIRATION = Symbol('ACCESS_TOKEN_EXPIRATION'); const REFRESH_TOKEN_SECRET = Symbol('REFRESH_TOKEN_SECRET'); const REFRESH_TOKEN_EXPIRATION = Symbol('REFRESH_TOKEN_EXPIRATION'); const ENABLE_GLOBAL_GUARD = Symbol('ENABLE_GLOBAL_GUARD'); const ONLY_RESET_REFRESH_TOKEN_EXPIRATION_BY_PASSWORD = Symbol('ONLY_RESET_REFRESH_TOKEN_EXPIRATION_BY_PASSWORD'); const COOKIE_MODE = Symbol('COOKIE_MODE'); const LOGIN_FAILED_AUTO_UNLOCK_SECONDS = Symbol('LOGIN_FAILED_AUTO_UNLOCK_SECONDS'); // Options Entity Providers const PROVIDE_MEMBER_ENTITY = Symbol('PROVIDE_MEMBER_ENTITY'); // Resolved Entity Repository Providers const RESOLVED_MEMBER_REPO = Symbol('RESOLVED_MEMBER_REPO'); // Password Policy Providers const PASSWORD_SHOULD_INCLUDE_UPPERCASE = Symbol('PASSWORD_SHOULD_INCLUDE_UPPERCASE'); const PASSWORD_SHOULD_INCLUDE_LOWERCASE = Symbol('PASSWORD_SHOULD_INCLUDE_LOWERCASE'); const PASSWORD_SHOULD_INCLUDE_DIGIT = Symbol('PASSWORD_SHOULD_INCLUDE_DIGIT'); const PASSWORD_SHOULD_INCLUDE_SPECIAL_CHARACTER = Symbol('PASSWORD_SHOULD_INCLUDE_SPECIAL_CHARACTER'); const PASSWORD_MIN_LENGTH = Symbol('PASSWORD_MIN_LENGTH'); const PASSWORD_POLICY_REGEXP = Symbol('PASSWORD_POLICY_REGEXP'); const PASSWORD_HISTORY_LIMIT = Symbol('PASSWORD_HISTORY_LIMIT'); const PASSWORD_AGE_LIMIT_IN_DAYS = Symbol('PASSWORD_AGE_LIMIT_IN_DAYS'); const FORCE_REJECT_LOGIN_ON_PASSWORD_EXPIRED = Symbol('FORCE_REJECT_LOGIN_ON_PASSWORD_EXPIRED'); const CUSTOMIZED_JWT_PAYLOAD = Symbol('CUSTOMIZED_JWT_PAYLOAD'); // OAuth2 Providers const OAUTH2_PROVIDERS = Symbol('OAUTH2_PROVIDERS'); const OAUTH2_CLIENT_DEST_URL = Symbol('OAUTH2_CLIENT_DEST_URL'); class MemberNotFoundError extends common.BadRequestException { constructor(){ super('Member not found'); } code = 100; } class PasswordDoesNotMeetPolicyError extends common.BadRequestException { constructor(){ super('Password does not meet the policy'); } code = 101; } class InvalidPasswordError extends common.BadRequestException { constructor(){ super('Invalid password'); } code = 102; } class PasswordValidationError extends common.InternalServerErrorException { constructor(){ super('Password validation error'); } code = 103; } class InvalidToken extends common.BadRequestException { constructor(){ super('Invalid token'); } code = 104; } class MemberAlreadyExistedError extends common.BadRequestException { constructor(){ super('Member already existed'); } code = 105; } class PasswordChangedError extends common.BadRequestException { constructor(){ super('Password changed, please sign in again'); } code = 106; } class MemberBannedError extends common.BadRequestException { constructor(){ super('Member banned'); } code = 107; } class PasswordExpiredError extends common.BadRequestException { constructor(){ super('Password expired, please update password'); } code = 108; } class PasswordShouldUpdatePasswordError extends common.BadRequestException { constructor(){ super('Member should update password before login'); } code = 109; } class PasswordInHistoryError extends common.BadRequestException { constructor(){ super('Password is in history'); } code = 110; } function _ts_decorate$9(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_param$5(paramIndex, decorator) { return function(target, key) { decorator(target, key, paramIndex); }; } 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() { return generatePassword.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 = luxon.DateTime.fromJSDate(member.passwordChangedAt).plus({ days: this.passwordAgeLimitInDays }).endOf('day').toMillis(); return validBefore < luxon.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 argon2.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$9([ common.Injectable(), _ts_param$5(0, common.Inject(PASSWORD_SHOULD_INCLUDE_UPPERCASE)), _ts_param$5(1, common.Inject(PASSWORD_SHOULD_INCLUDE_LOWERCASE)), _ts_param$5(2, common.Inject(PASSWORD_SHOULD_INCLUDE_DIGIT)), _ts_param$5(3, common.Inject(PASSWORD_SHOULD_INCLUDE_SPECIAL_CHARACTER)), _ts_param$5(4, common.Inject(PASSWORD_MIN_LENGTH)), _ts_param$5(5, common.Inject(PASSWORD_POLICY_REGEXP)), _ts_param$5(6, common.Inject(PASSWORD_HISTORY_LIMIT)), _ts_param$5(7, common.Inject(MemberPasswordHistoryRepo)), _ts_param$5(8, common.Inject(PASSWORD_AGE_LIMIT_IN_DAYS)) ], PasswordValidatorService); function _ts_decorate$8(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_param$4(paramIndex, decorator) { return function(target, key) { decorator(target, key, paramIndex); }; } class MemberBaseService { originalProvidedOptions; baseMemberRepo; memberLoginLogRepo; loginFailedBanThreshold; resetPasswordTokenExpiration; resetPasswordTokenSecret; accessTokenSecret; accessTokenExpiration; refreshTokenSecret; refreshTokenExpiration; onlyResetRefreshTokenExpirationByPassword; memberPasswordHistoryRepo; passwordAgeLimitInDays; forceRejectLoginOnPasswordExpired; passwordValidatorService; customizedJwtPayload; loginFailedAutoUnlockSeconds; constructor(originalProvidedOptions, baseMemberRepo, memberLoginLogRepo, loginFailedBanThreshold, resetPasswordTokenExpiration, resetPasswordTokenSecret, accessTokenSecret, accessTokenExpiration, refreshTokenSecret, refreshTokenExpiration, onlyResetRefreshTokenExpirationByPassword, memberPasswordHistoryRepo, passwordAgeLimitInDays, forceRejectLoginOnPasswordExpired, passwordValidatorService, customizedJwtPayload, loginFailedAutoUnlockSeconds){ this.originalProvidedOptions = originalProvidedOptions; this.baseMemberRepo = baseMemberRepo; this.memberLoginLogRepo = memberLoginLogRepo; this.loginFailedBanThreshold = loginFailedBanThreshold; this.resetPasswordTokenExpiration = resetPasswordTokenExpiration; this.resetPasswordTokenSecret = resetPasswordTokenSecret; this.accessTokenSecret = accessTokenSecret; this.accessTokenExpiration = accessTokenExpiration; this.refreshTokenSecret = refreshTokenSecret; this.refreshTokenExpiration = refreshTokenExpiration; this.onlyResetRefreshTokenExpirationByPassword = onlyResetRefreshTokenExpirationByPassword; this.memberPasswordHistoryRepo = memberPasswordHistoryRepo; this.passwordAgeLimitInDays = passwordAgeLimitInDays; this.forceRejectLoginOnPasswordExpired = forceRejectLoginOnPasswordExpired; this.passwordValidatorService = passwordValidatorService; this.customizedJwtPayload = customizedJwtPayload; this.loginFailedAutoUnlockSeconds = loginFailedAutoUnlockSeconds; this.logger = new common.Logger(MemberBaseService.name); } logger; onApplicationBootstrap() { if (!this.originalProvidedOptions?.accessTokenSecret || !this.originalProvidedOptions?.refreshTokenSecret) { this.logger.warn('No access token secret or refresh token secret provided, using random secret'); } } signRefreshToken(member, domain) { return jsonwebtoken.sign({ ...this.customizedJwtPayload(member), passwordChangedAt: member.passwordChangedAt?.getTime() ?? null, ...domain ? { domain } : {} }, this.refreshTokenSecret, { expiresIn: this.validateExpiration(this.refreshTokenExpiration, 'REFRESH_TOKEN_EXPIRATION') }); } signAccessToken(member, domain) { return jsonwebtoken.sign({ ...this.customizedJwtPayload(member), ...domain ? { domain } : {} }, this.accessTokenSecret, { expiresIn: this.validateExpiration(this.accessTokenExpiration, 'ACCESS_TOKEN_EXPIRATION') }); } async getResetPasswordToken(account) { const member = await this.baseMemberRepo.findOne({ where: { account } }); if (!member) { throw new MemberNotFoundError(); } const requestedOn = new Date(); member.resetPasswordRequestedAt = requestedOn; await this.baseMemberRepo.save(member); const token = jsonwebtoken.sign({ id: member.id, requestedOn: requestedOn.getTime() }, this.resetPasswordTokenSecret, { expiresIn: this.validateExpiration(this.resetPasswordTokenExpiration, 'RESET_PASSWORD_TOKEN_EXPIRATION') }); return token; } async changePassword(id, originPassword, newPassword) { if (!await this.passwordValidatorService.validatePassword(newPassword, id)) { throw new PasswordDoesNotMeetPolicyError(); } const member = await this.baseMemberRepo.findOne({ where: { id } }); if (!member) { throw new MemberNotFoundError(); } try { if (await argon2.verify(member.password, originPassword)) { member.password = await argon2.hash(newPassword); member.passwordChangedAt = new Date(); member.shouldUpdatePassword = false; await this.baseMemberRepo.save(member); await this.memberPasswordHistoryRepo.save(this.memberPasswordHistoryRepo.create({ memberId: member.id, password: member.password })); return member; } else { throw new InvalidPasswordError(); } } catch (err) { throw new PasswordValidationError(); } } async changePasswordWithToken(token, newPassword) { try { const { id, requestedOn } = jsonwebtoken.verify(token, this.resetPasswordTokenSecret); if (!await this.passwordValidatorService.validatePassword(newPassword, id)) { throw new PasswordDoesNotMeetPolicyError(); } const member = await this.baseMemberRepo.findOne({ where: { id, resetPasswordRequestedAt: new Date(requestedOn) } }); if (!member) { throw new InvalidToken(); } member.password = await argon2.hash(newPassword); member.passwordChangedAt = new Date(); member.shouldUpdatePassword = false; member.resetPasswordRequestedAt = null; await this.baseMemberRepo.save(member); await this.memberPasswordHistoryRepo.save(this.memberPasswordHistoryRepo.create({ memberId: member.id, password: member.password })); return member; } catch (ex) { throw new InvalidToken(); } } async register(account, password, memberOptions) { if (!await this.passwordValidatorService.validatePassword(password)) { throw new PasswordDoesNotMeetPolicyError(); } let member = this.baseMemberRepo.create({ account }); member.password = await argon2.hash(password); try { member = await this.baseMemberRepo.save({ ...memberOptions, account: member.account, password: member.password }); await this.memberPasswordHistoryRepo.save(this.memberPasswordHistoryRepo.create({ memberId: member.id, password: member.password })); } catch (ex) { if (/unique/.test(ex.message)) { throw new MemberAlreadyExistedError(); } } return member; } async registerWithoutPassword(account, memberOptions) { const password = this.passwordValidatorService.generateValidPassword(); let member = this.baseMemberRepo.create({ account }); member.password = await argon2.hash(password); try { member = await this.baseMemberRepo.save({ shouldUpdatePassword: true, ...memberOptions, account: member.account, password: member.password }); await this.memberPasswordHistoryRepo.save(this.memberPasswordHistoryRepo.create({ memberId: member.id, password: member.password })); } catch (ex) { if (/unique/.test(ex.message)) { throw new MemberAlreadyExistedError(); } } return [ member, password ]; } async refreshToken(refreshToken, options) { try { const { id, account, passwordChangedAt, exp } = jsonwebtoken.verify(refreshToken, this.refreshTokenSecret); const member = await this.baseMemberRepo.findOne({ where: { id, account } }); if (!member) { throw new MemberNotFoundError(); } if (member.passwordChangedAt?.getTime() !== passwordChangedAt) { throw new PasswordChangedError(); } return { accessToken: this.signAccessToken(member, options?.domain ?? undefined), refreshToken: this.signRefreshToken(member, options?.domain ?? undefined) }; } catch (ex) { if (ex instanceof common.BadRequestException) throw ex; throw new InvalidToken(); } } async login(account, password, options) { const member = await this.baseMemberRepo.findOne({ where: { account } }); if (!member) { throw new MemberNotFoundError(); } if (member.loginFailedCounter >= this.loginFailedBanThreshold) { if (this.loginFailedAutoUnlockSeconds) { const latestFailedRecord = await this.memberLoginLogRepo.findOne({ order: { createdAt: 'DESC' }, where: { memberId: member.id, success: false } }); if (!latestFailedRecord || latestFailedRecord.createdAt.getTime() + this.loginFailedAutoUnlockSeconds * 1000 > Date.now()) { throw new MemberBannedError(); } } else { throw new MemberBannedError(); } } const isPasswordExpired = this.passwordAgeLimitInDays ? this.passwordValidatorService.shouldUpdatePassword(member) : false; if (isPasswordExpired && this.forceRejectLoginOnPasswordExpired) { throw new PasswordExpiredError(); } const ip = typeof options === 'string' ? options : options?.ip ?? undefined; try { if (await argon2.verify(member.password, password)) { if (member.shouldUpdatePassword) { throw new PasswordShouldUpdatePasswordError(); } member.loginFailedCounter = 0; await this.baseMemberRepo.save(member); // async log this.memberLoginLogRepo.save({ memberId: member.id, success: true, ip: ip ? `${ip}/32` : null }); const domain = typeof options === 'string' ? undefined : options?.domain ?? undefined; return { accessToken: this.signAccessToken(member, domain), refreshToken: this.signRefreshToken(member, domain), ...this.passwordAgeLimitInDays ? { shouldUpdatePassword: isPasswordExpired, passwordChangedAt: member.passwordChangedAt?.toISOString() } : {} }; } member.loginFailedCounter += 1; await this.baseMemberRepo.save(member); // async log this.memberLoginLogRepo.save({ memberId: member.id, success: false, ip: ip ? `${ip}/32` : null }); throw new InvalidPasswordError(); } catch (err) { if (err instanceof common.BadRequestException) throw err; throw new PasswordValidationError(); } } async resetLoginFailedCounter(id) { const member = await this.baseMemberRepo.findOne({ where: { id } }); if (!member) { throw new MemberNotFoundError(); } member.loginFailedCounter = 0; await this.baseMemberRepo.save(member); return member; } validateExpiration(source, tokenName) { if (typeof source !== 'number' || Number.isNaN(source)) { throw new common.BadRequestException(`[${tokenName}] must be a number (in seconds), but got: ${source}`); } return source; } } MemberBaseService = _ts_decorate$8([ common.Injectable(), _ts_param$4(0, common.Inject(MEMBER_BASE_MODULE_OPTIONS)), _ts_param$4(1, common.Inject(RESOLVED_MEMBER_REPO)), _ts_param$4(2, common.Inject(MemberLoginLogRepo)), _ts_param$4(3, common.Inject(LOGIN_FAILED_BAN_THRESHOLD)), _ts_param$4(4, common.Inject(RESET_PASSWORD_TOKEN_EXPIRATION)), _ts_param$4(5, common.Inject(RESET_PASSWORD_TOKEN_SECRET)), _ts_param$4(6, common.Inject(ACCESS_TOKEN_SECRET)), _ts_param$4(7, common.Inject(ACCESS_TOKEN_EXPIRATION)), _ts_param$4(8, common.Inject(REFRESH_TOKEN_SECRET)), _ts_param$4(9, common.Inject(REFRESH_TOKEN_EXPIRATION)), _ts_param$4(10, common.Inject(ONLY_RESET_REFRESH_TOKEN_EXPIRATION_BY_PASSWORD)), _ts_param$4(11, common.Inject(MemberPasswordHistoryRepo)), _ts_param$4(12, common.Inject(PASSWORD_AGE_LIMIT_IN_DAYS)), _ts_param$4(13, common.Inject(FORCE_REJECT_LOGIN_ON_PASSWORD_EXPIRED)), _ts_param$4(14, common.Inject(PasswordValidatorService)), _ts_param$4(15, common.Inject(CUSTOMIZED_JWT_PAYLOAD)), _ts_param$4(16, common.Inject(LOGIN_FAILED_AUTO_UNLOCK_SECONDS)) ], MemberBaseService); function _ts_decorate$7(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_param$3(paramIndex, decorator) { return function(target, key) { decorator(target, key, paramIndex); }; } class MemberBaseAdminService { baseMemberRepo; passwordValidatorService; memberPasswordHistoryRepo; constructor(baseMemberRepo, passwordValidatorService, memberPasswordHistoryRepo){ this.baseMemberRepo = baseMemberRepo; this.passwordValidatorService = passwordValidatorService; this.memberPasswordHistoryRepo = memberPasswordHistoryRepo; } async archiveMember(id) { const member = await this.baseMemberRepo.findOne({ where: { id } }); if (!member) { throw new MemberNotFoundError(); } await this.baseMemberRepo.softRemove(member); } async resetMemberPassword(id, newPassword, ignorePasswordPolicy = false) { if (!ignorePasswordPolicy && !await this.passwordValidatorService.validatePassword(newPassword, id)) { throw new PasswordDoesNotMeetPolicyError(); } const member = await this.baseMemberRepo.findOne({ where: { id } }); if (!member) { throw new MemberNotFoundError(); } member.password = await argon2.hash(newPassword); member.passwordChangedAt = new Date(); await this.baseMemberRepo.save(member); await this.memberPasswordHistoryRepo.save(this.memberPasswordHistoryRepo.create({ memberId: member.id, password: member.password })); return member; } } MemberBaseAdminService = _ts_decorate$7([ common.Injectable(), _ts_param$3(0, common.Inject(RESOLVED_MEMBER_REPO)), _ts_param$3(1, common.Inject(PasswordValidatorService)), _ts_param$3(2, common.Inject(MemberPasswordHistoryRepo)) ], MemberBaseAdminService); const TARGETS = [ [ BaseMemberRepo, PROVIDE_MEMBER_ENTITY, RESOLVED_MEMBER_REPO ] ]; const ResolvedRepoProviders = TARGETS.map(([repo, provide, resolved])=>({ provide: resolved, useFactory: (baseRepo, entity, dataSource)=>entity ? dataSource.getRepository(entity) : baseRepo, inject: [ repo, provide, typeorm.DataSource ] })); const CASBIN_MODEL = ` [request_definition] r = sub, dom, obj, act [policy_definition] p = sub, dom, obj, act [role_definition] g = _, _, _ [policy_effect] e = some(where (p.eft == allow)) [matchers] m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act `; const AllowActions = core.Reflector.createDecorator(); const DEFAULT_CASBIN_DOMAIN = '::DEFAULT::'; const TypeORMAdapter = require('typeorm-adapter').default; const OptionProviders = [ { provide: LOGIN_FAILED_BAN_THRESHOLD, useFactory: (options)=>options?.loginFailedBanThreshold ?? 5, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: RESET_PASSWORD_TOKEN_EXPIRATION, useFactory: (options)=>options?.resetPasswordTokenExpiration ?? 60 * 60 * 1, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: RESET_PASSWORD_TOKEN_SECRET, useFactory: (options)=>options?.resetPasswordTokenSecret ?? node_crypto.randomBytes(16).toString('hex'), inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: PROVIDE_MEMBER_ENTITY, useFactory: (options)=>options?.memberEntity ?? null, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: ACCESS_TOKEN_SECRET, useFactory: (options)=>options?.accessTokenSecret ?? node_crypto.randomBytes(16).toString('hex'), inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: ACCESS_TOKEN_EXPIRATION, useFactory: (options)=>options?.accessTokenExpiration ?? 60 * 15, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: REFRESH_TOKEN_SECRET, useFactory: (options)=>options?.refreshTokenSecret ?? node_crypto.randomBytes(16).toString('hex'), inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: REFRESH_TOKEN_EXPIRATION, useFactory: (options)=>options?.refreshTokenExpiration ?? 60 * 60 * 24 * 90, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: ENABLE_GLOBAL_GUARD, useFactory: (options)=>options?.enableGlobalGuard ?? true, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: CASBIN_ENFORCER, useFactory: async (options)=>{ if (!options?.casbinAdapterOptions) return null; const adapter = await TypeORMAdapter.newAdapter(options.casbinAdapterOptions); const enforcer = await casbin.newEnforcer(); const model = casbin.newModelFromString(options.casbinModelString || CASBIN_MODEL); await enforcer.initWithModelAndAdapter(model, adapter); await enforcer.loadPolicy(); return enforcer; }, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: CASBIN_PERMISSION_DECORATOR, useFactory: async (options)=>options?.casbinPermissionDecorator ?? AllowActions, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: CASBIN_PERMISSION_CHECKER, useFactory: async (options)=>options?.casbinPermissionChecker ? options.casbinPermissionChecker : ({ enforcer, payload, actions })=>Promise.all(actions.map(([subject, action])=>enforcer.enforce(payload.id, payload.domain ?? DEFAULT_CASBIN_DOMAIN, subject, action))).then((results)=>results.some((result)=>result)), inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: PASSWORD_SHOULD_INCLUDE_UPPERCASE, useFactory: (options)=>options?.passwordShouldIncludeUppercase ?? true, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: PASSWORD_SHOULD_INCLUDE_LOWERCASE, useFactory: (options)=>options?.passwordShouldIncludeLowercase ?? true, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: PASSWORD_SHOULD_INCLUDE_DIGIT, useFactory: (options)=>options?.passwordShouldIncludeDigit ?? true, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: PASSWORD_SHOULD_INCLUDE_SPECIAL_CHARACTER, useFactory: (options)=>options?.passwordShouldIncludeSpecialCharacters ?? false, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: PASSWORD_MIN_LENGTH, useFactory: (options)=>options?.passwordMinLength ?? 8, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: PASSWORD_POLICY_REGEXP, useFactory: (options)=>options?.passwordPolicyRegExp ?? undefined, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: PASSWORD_HISTORY_LIMIT, useFactory: (options)=>options?.passwordHistoryLimit ?? undefined, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: PASSWORD_AGE_LIMIT_IN_DAYS, useFactory: (options)=>options?.passwordAgeLimitInDays ?? undefined, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: ONLY_RESET_REFRESH_TOKEN_EXPIRATION_BY_PASSWORD, useFactory: (options)=>options?.onlyResetRefreshTokenExpirationByPassword ?? false, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: FORCE_REJECT_LOGIN_ON_PASSWORD_EXPIRED, useFactory: (options)=>options?.forceRejectLoginOnPasswordExpired ?? false, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: CUSTOMIZED_JWT_PAYLOAD, useFactory: (options)=>options?.customizedJwtPayload ?? ((member)=>({ id: member.id, account: member.account })) }, { provide: OAUTH2_PROVIDERS, useFactory: (options)=>options?.oauth2Providers ?? [], inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: OAUTH2_CLIENT_DEST_URL, useFactory: (options)=>options?.oauth2ClientDestUrl ?? '/login', inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: COOKIE_MODE, useFactory: (options)=>options?.cookieMode ?? false, inject: [ MEMBER_BASE_MODULE_OPTIONS ] }, { provide: LOGIN_FAILED_AUTO_UNLOCK_SECONDS, useFactory: (options)=>options?.loginFailedAutoUnlockSeconds ?? null, inject: [ MEMBER_BASE_MODULE_OPTIONS ] } ]; const IS_ROUTE_PUBLIC = 'IS_ROUTE_PUBLIC'; const IsPublic = ()=>common.SetMetadata(IS_ROUTE_PUBLIC, true); const IS_ROUTE_ONLY_AUTHENTICATED = 'IS_ROUTE_ONLY_AUTHENTICATED'; const Authenticated = ()=>common.SetMetadata(IS_ROUTE_ONLY_AUTHENTICATED, true); function _ts_decorate$6(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_param$2(paramIndex, decorator) { return function(target, key) { decorator(target, key, paramIndex); }; } class CasbinGuard { cookieMode; enforcer; accessTokenSecret; refreshTokenSecret; enableGlobalGuard; permissionDecorator; permissionChecker; constructor(cookieMode, enforcer, accessTokenSecret, refreshTokenSecret, enableGlobalGuard, permissionDecorator, permissionChecker){ this.cookieMode = cookieMode; this.enforcer = enforcer; this.accessTokenSecret = accessTokenSecret; this.refreshTokenSecret = refreshTokenSecret; this.enableGlobalGuard = enableGlobalGuard; this.permissionDecorator = permissionDecorator; this.permissionChecker = permissionChecker; } async canActivate(context) { if (!this.enableGlobalGuard) return true; const reflector = new core.Reflector(); const isPublic = reflector.get(IS_ROUTE_PUBLIC, context.getHandler()); const onlyAuthenticated = reflector.get(IS_ROUTE_ONLY_AUTHENTICATED, context.getHandler()); if (isPublic) return true; const allowActions = reflector.get(this.permissionDecorator ?? AllowActions, context.getHandler()); if (!allowActions?.length && !onlyAuthenticated) return false; const contextType = context.getType(); let token; switch(contextType){ case 'graphql': { const { GqlExecutionContext } = await import('@nestjs/graphql'); const ctx = GqlExecutionContext.create(context).getContext(); token = (this.cookieMode ? ctx.req.cookies.token : (ctx.req.headers.authorization ?? '').replace(/^Bearer\s/, '').trim()) ?? null; break; } case 'http': default: token = (this.cookieMode ? context.switchToHttp().getRequest().cookies.token : context.switchToHttp().getRequest().headers.authorization ?? '').replace(/^Bearer\s/, '').trim(); break; } if (!token) return false; try { const payload = jsonwebtoken.verify(token, this.cookieMode ? this.refreshTokenSecret : this.accessTokenSecret); switch(contextType){ case 'graphql': { const { GqlExecutionContext } = await import('@nestjs/graphql'); const ctx = GqlExecutionContext.create(context).getContext(); ctx.payload = payload; break; } case 'http': default: context.switchToHttp().getRequest().payload = payload; break; } if (onlyAuthenticated) return true; return this.permissionChecker({ enforcer: this.enforcer, payload, actions: allowActions }); } catch (ex) { return false; } } } CasbinGuard = _ts_decorate$6([ common.Injectable(), _ts_param$2(0, common.Inject(COOKIE_MODE)), _ts_param$2(1, common.Inject(CASBIN_ENFORCER)), _ts_param$2(2, common.Inject(ACCESS_TOKEN_SECRET)), _ts_param$2(3, common.Inject(REFRESH_TOKEN_SECRET)), _ts_param$2(4, common.Inject(ENABLE_GLOBAL_GUARD)), _ts_param$2(5, common.Inject(CASBIN_PERMISSION_DECORATOR)), _ts_param$2(6, common.Inject(CASBIN_PERMISSION_CHECKER)) ], CasbinGuard); function _ts_decorate$5(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_param$1(paramIndex, decorator) { return function(target, key) { decorator(target, key, paramIndex); }; } class OAuthService { providers; baseMemberRepo; memberOAuthRecordRepo; memberBaseService; constructor(providers, baseMemberRepo, memberOAuthRecordRepo, memberBaseService){ this.providers = providers; this.baseMemberRepo = baseMemberRepo; this.memberOAuthRecordRepo = memberOAuthRecordRepo; this.memberBaseService = memberBaseService; } async getGoogleOAuthLoginUrl() { const provider = this.providers.find((p)=>p.channel === 'google'); if (!provider) { throw new common.BadRequestException('Google OAuth2 provider not found'); } const state = await provider.getState?.() ?? null; const params = new URLSearchParams({ client_id: provider.clientId, redirect_uri: provider.redirectUri, response_type: 'code', scope: (provider.scope ?? [ 'openid', 'email' ]).join(' '), ...state ? { state } : {} }); return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`; } async getFacebookOAuthLoginUrl() { const provider = this.providers.find((p)=>p.channel === 'facebook'); if (!provider) { throw new common.BadRequestException('Facebook OAuth2 provider not found'); } const state = await provider.getState?.() ?? null; const params = new URLSearchParams({ client_id: provider.clientId, redirect_uri: provider.redirectUri, response_type: 'code', scope: (provider.scope ?? [ 'public_profile', 'email' ]).join(' '), ...state ? { state } : {} }); return `https://www.facebook.com/v22.0/dialog/oauth?${params.toString()}`; } async getCustomOAuthLoginUrl(channel) { const provider = this.providers.find((p)=>p.channel === channel); if (!provider) { throw new common.BadRequestException('OAuth2 provider not found'); } const state = await provider.getState?.() ?? null; const params = new URLSearchParams({ client_id: provider.clientId, redirect_uri: provider.redirectUri, response_type: 'code', scope: provider.scope.join(' '), ...state ? { state } : {} }); return `${provider.requestUrl}?${params.toString()}`; } async loginWithGoogleOAuth2Code(code, state) { const provider = this.providers.find((p)=>p.channel === 'google'); if (!provider) { throw new common.BadRequestException('Google OAuth2 provider not found'); } const { data: { access_token } } = await axios.post('https://oauth2.googleapis.com/token', { code, client_id: provider.clientId, client_secret: provider.clientSecret, redirect_uri: provider.redirectUri, grant_type: 'authorization_code' }); const { data } =