diff --git a/modules/authentication/src/Authentication.ts b/modules/authentication/src/Authentication.ts index f73723240..f65b2e796 100644 --- a/modules/authentication/src/Authentication.ts +++ b/modules/authentication/src/Authentication.ts @@ -6,7 +6,6 @@ import ConduitGrpcSdk, { GrpcCallback, HealthCheckStatus, } from '@conduitplatform/grpc-sdk'; - import path from 'path'; import { isNil } from 'lodash'; import { status } from '@grpc/grpc-js'; @@ -46,7 +45,8 @@ export default class Authentication extends ManagedModule { private adminRouter: AdminHandlers; private userRouter: AuthenticationRoutes; private database: DatabaseProvider; - private sendEmail: boolean = false; + private localSendVerificationEmail: boolean = false; + private refreshAppRoutesTimeout: NodeJS.Timeout | null = null; constructor() { super('authentication'); @@ -59,20 +59,7 @@ export default class Authentication extends ManagedModule { await runMigrations(this.grpcSdk); } - async onRegister() { - this.grpcSdk.bus!.subscribe('email:status:onConfig', () => { - this.onConfig() - .then(() => { - ConduitGrpcSdk.Logger.log('Updated authentication configuration'); - }) - .catch(() => { - ConduitGrpcSdk.Logger.error('Failed to update authentication config'); - }); - }); - } - protected registerSchemas() { - // @ts-ignore const promises = Object.values(models).map(model => { const modelInstance = model.getInstance(this.database); return this.database.createSchemaFromAdapter(modelInstance); @@ -85,28 +72,35 @@ export default class Authentication extends ManagedModule { if (!config.active) { this.updateHealth(HealthCheckStatus.NOT_SERVING); } else { - let refreshedOnce = false; await this.registerSchemas(); + this.adminRouter = new AdminHandlers(this.grpcServer, this.grpcSdk); + await this.refreshAppRoutes(); + this.updateHealth(HealthCheckStatus.SERVING); if (config.local.verification.send_email) { - this.grpcSdk.monitorModule('email', serving => { - this.sendEmail = serving; - this.refreshAppRoutes(); - refreshedOnce = true; + this.grpcSdk.monitorModule('email', async serving => { + const alreadyEnabled = this.localSendVerificationEmail; + this.localSendVerificationEmail = serving; + if (serving) { + if (!alreadyEnabled) { + await this.refreshAppRoutes(); + } + } else { + ConduitGrpcSdk.Logger.warn( + 'Failed to enable email verification for local authentication strategy. Email module not serving.', + ); + } }); } else { + this.localSendVerificationEmail = false; this.grpcSdk.unmonitorModule('email'); } - this.adminRouter = new AdminHandlers(this.grpcServer, this.grpcSdk); - if (!refreshedOnce) { - await this.refreshAppRoutes(); - } - this.updateHealth(HealthCheckStatus.SERVING); } } private async refreshAppRoutes() { if (this.userRouter) { - await this.userRouter.registerRoutes(); + this.userRouter.updateLocalHandlers(this.localSendVerificationEmail); + this.scheduleAppRouteRefresh(); return; } const self = this; @@ -116,15 +110,30 @@ export default class Authentication extends ManagedModule { self.userRouter = new AuthenticationRoutes( self.grpcServer, self.grpcSdk, - self.sendEmail, + self.localSendVerificationEmail, ); - return this.userRouter.registerRoutes(); + this.scheduleAppRouteRefresh(); }) .catch(e => { ConduitGrpcSdk.Logger.error(e.message); }); } + private scheduleAppRouteRefresh() { + if (this.refreshAppRoutesTimeout) { + clearTimeout(this.refreshAppRoutesTimeout); + this.refreshAppRoutesTimeout = null; + } + this.refreshAppRoutesTimeout = setTimeout(async () => { + try { + await this.userRouter.registerRoutes(); + } catch (err) { + ConduitGrpcSdk.Logger.error(err as Error); + } + this.refreshAppRoutesTimeout = null; + }, 800); + } + // gRPC Service // produces login credentials for a user without them having to login async userLogin( @@ -207,7 +216,7 @@ export default class Authentication extends ManagedModule { isVerified: !verify, }); - if (verify && this.sendEmail) { + if (verify && this.localSendVerificationEmail) { const serverConfig = await this.grpcSdk.config.get('router'); const url = serverConfig.hostUrl; const verificationToken: models.Token = await models.Token.getInstance().create({ diff --git a/modules/authentication/src/handlers/local.ts b/modules/authentication/src/handlers/local.ts index 3b9550407..bd0da1608 100644 --- a/modules/authentication/src/handlers/local.ts +++ b/modules/authentication/src/handlers/local.ts @@ -3,9 +3,7 @@ import { AuthUtils } from '../utils/auth'; import { TokenType } from '../constants/TokenType'; import { v4 as uuid } from 'uuid'; import { Config } from '../config'; - import ConduitGrpcSdk, { - ConduitError, Email, GrpcError, ParsedRouterRequest, @@ -31,7 +29,7 @@ export class LocalHandlers implements IAuthenticationStrategy { constructor( private readonly grpcSdk: ConduitGrpcSdk, - private readonly sendEmail: boolean, + private readonly sendVerificationEmail: boolean, ) { grpcSdk.config.get('router').then(config => { this.clientValidation = config.security.clientValidation; @@ -77,7 +75,7 @@ export class LocalHandlers implements IAuthenticationStrategy { this.authenticate.bind(this), ); - if (this.emailModule) { + if (this.sendVerificationEmail) { routingManager.route( { path: '/forgot-password', @@ -214,23 +212,6 @@ export class LocalHandlers implements IAuthenticationStrategy { } async validate(): Promise { - if (this.sendEmail) { - let emailConfig; - try { - emailConfig = await this.grpcSdk.config.get('email'); - } catch (e) { - ConduitGrpcSdk.Logger.log( - 'Cannot use email verification without Email module being enabled', - ); - return (this.initialized = false); - } - if (!emailConfig.active) { - ConduitGrpcSdk.Logger.log( - 'Cannot use email verification without Email module being enabled', - ); - return (this.initialized = false); - } - } if (!this.initialized) { try { await this.initDbAndEmail(); @@ -270,7 +251,7 @@ export class LocalHandlers implements IAuthenticationStrategy { const serverConfig = await this.grpcSdk.config.get('router'); const url = serverConfig.hostUrl; - if (this.sendEmail) { + if (this.sendVerificationEmail) { const verificationToken: Token = await Token.getInstance().create({ type: TokenType.VERIFICATION_TOKEN, userId: user._id, @@ -278,15 +259,13 @@ export class LocalHandlers implements IAuthenticationStrategy { }); const result = { verificationToken, hostUrl: url }; const link = `${result.hostUrl}/hook/authentication/verify-email/${result.verificationToken.token}`; - if (this.sendEmail) { - await this.emailModule.sendEmail('EmailVerification', { - email: user.email, - sender: 'no-reply', - variables: { - link, - }, - }); - } + await this.emailModule.sendEmail('EmailVerification', { + email: user.email, + sender: 'no-reply', + variables: { + link, + }, + }); } delete user.hashedPassword; return { user }; @@ -433,7 +412,7 @@ export class LocalHandlers implements IAuthenticationStrategy { const appUrl = config.local.forgot_password_redirect_uri; const link = `${appUrl}?reset_token=${passwordResetTokenDoc.token}`; - if (this.sendEmail) { + if (this.sendVerificationEmail) { await this.emailModule.sendEmail('ForgotPassword', { email: user.email, sender: 'no-reply', @@ -765,7 +744,7 @@ export class LocalHandlers implements IAuthenticationStrategy { private async initDbAndEmail() { const config = ConfigController.getInstance().config; - if (this.sendEmail) { + if (this.sendVerificationEmail) { await this.grpcSdk.config.moduleExists('email'); this.emailModule = this.grpcSdk.emailProvider!; } @@ -788,7 +767,7 @@ export class LocalHandlers implements IAuthenticationStrategy { ConduitGrpcSdk.Logger.log('phone authentication not active'); } - if (this.sendEmail) { + if (this.sendVerificationEmail) { this.registerTemplates(); } this.initialized = true; diff --git a/modules/authentication/src/routes/index.ts b/modules/authentication/src/routes/index.ts index d1277e08e..59c908007 100644 --- a/modules/authentication/src/routes/index.ts +++ b/modules/authentication/src/routes/index.ts @@ -24,7 +24,7 @@ import { OAuth2Settings } from '../handlers/oauth2/interfaces/OAuth2Settings'; type OAuthHandler = typeof oauth2; export class AuthenticationRoutes { - private readonly localHandlers: LocalHandlers; + private localHandlers: LocalHandlers; private readonly serviceHandler: ServiceHandler; private readonly commonHandlers: CommonHandlers; private readonly phoneHandlers: PhoneHandlers; @@ -33,13 +33,18 @@ export class AuthenticationRoutes { constructor( readonly server: GrpcServer, private readonly grpcSdk: ConduitGrpcSdk, - private readonly emailServing: boolean, + private localSendVerificationEmail: boolean, ) { this._routingManager = new RoutingManager(this.grpcSdk.router!, server); - this.localHandlers = new LocalHandlers(grpcSdk, emailServing); this.serviceHandler = new ServiceHandler(grpcSdk); this.commonHandlers = new CommonHandlers(grpcSdk); this.phoneHandlers = new PhoneHandlers(grpcSdk); + this.updateLocalHandlers(localSendVerificationEmail); + } + + updateLocalHandlers(sendVerificationEmail: boolean) { + this.localSendVerificationEmail = sendVerificationEmail; + this.localHandlers = new LocalHandlers(this.grpcSdk, sendVerificationEmail); } async registerRoutes() {