@rytass/member-base-nestjs-module
Version:
Rytass Member System NestJS Base Module
1,292 lines (1,268 loc) • 68.8 kB
JavaScript
'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 } =