Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(api-service): centralize auth into @novu/api and reduce DI complexity #7640

Merged
merged 5 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions apps/api/src/app/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,7 @@ import { MemberEntity, MemberRepository, UserRepository } from '@novu/dal';
import { AuthGuard } from '@nestjs/passport';
import { PasswordResetFlowEnum, UserSessionData } from '@novu/shared';
import { ApiExcludeController, ApiTags } from '@nestjs/swagger';
import {
AuthService,
buildOauthRedirectUrl,
SwitchEnvironment,
SwitchEnvironmentCommand,
SwitchOrganization,
SwitchOrganizationCommand,
} from '@novu/application-generic';
import { buildOauthRedirectUrl } from '@novu/application-generic';
import { UserRegistrationBodyDto } from './dtos/user-registration.dto';
import { UserRegister } from './usecases/register/user-register.usecase';
import { UserRegisterCommand } from './usecases/register/user-register.command';
Expand All @@ -47,6 +40,11 @@ import { UpdatePasswordBodyDto } from './dtos/update-password.dto';
import { UpdatePassword } from './usecases/update-password/update-password.usecase';
import { UpdatePasswordCommand } from './usecases/update-password/update-password.command';
import { UserAuthentication } from '../shared/framework/swagger/api.key.security';
import { SwitchEnvironmentCommand } from './usecases/switch-environment/switch-environment.command';
import { SwitchEnvironment } from './usecases/switch-environment/switch-environment.usecase';
import { SwitchOrganizationCommand } from './usecases/switch-organization/switch-organization.command';
import { SwitchOrganization } from './usecases/switch-organization/switch-organization.usecase';
import { AuthService } from './services/auth.service';
Comment on lines +43 to +47
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These services are used only in API, no reason to have them in app-generic

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍


@ApiCommonResponses()
@Controller('/auth')
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/app/auth/community.auth.module.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { PassportModule } from '@nestjs/passport';
import passport from 'passport';

import { AuthProviderEnum, PassportStrategyEnum } from '@novu/shared';
import { AuthService, RolesGuard, injectCommunityAuthProviders } from '@novu/application-generic';

import { JwtStrategy } from './services/passport/jwt.strategy';
import { AuthController } from './auth.controller';
Expand All @@ -17,6 +16,9 @@ import { EnvironmentsModuleV1 } from '../environments-v1/environments-v1.module'
import { JwtSubscriberStrategy } from './services/passport/subscriber-jwt.strategy';
import { RootEnvironmentGuard } from './framework/root-environment-guard.service';
import { ApiKeyStrategy } from './services/passport/apikey.strategy';
import { injectCommunityAuthProviders } from './inject-auth-providers';
import { AuthService } from './services/auth.service';
import { RolesGuard } from './framework/roles.guard';

const AUTH_STRATEGIES: Provider[] = [JwtStrategy, ApiKeyStrategy, JwtSubscriberStrategy];

Expand Down
13 changes: 5 additions & 8 deletions apps/api/src/app/auth/ee.auth.module.config.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
/* eslint-disable global-require */
import {
AuthService,
SwitchEnvironment,
SwitchOrganization,
PlatformException,
cacheService,
RolesGuard,
} from '@novu/application-generic';
import { PlatformException, cacheService } from '@novu/application-generic';
import { MiddlewareConsumer, ModuleMetadata } from '@nestjs/common';
import { RootEnvironmentGuard } from './framework/root-environment-guard.service';
import { ApiKeyStrategy } from './services/passport/apikey.strategy';
import { JwtSubscriberStrategy } from './services/passport/subscriber-jwt.strategy';
import { OrganizationModule } from '../organization/organization.module';
import { SwitchEnvironment } from './usecases/switch-environment/switch-environment.usecase';
import { SwitchOrganization } from './usecases/switch-organization/switch-organization.usecase';
import { AuthService } from './services/auth.service';
import { RolesGuard } from './framework/roles.guard';

function getEEAuthProviders() {
const eeAuthPackage = require('@novu/ee-auth');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,14 @@
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard, IAuthModuleOptions } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import {
ApiAuthSchemeEnum,
IJwtClaims,
PassportStrategyEnum,
HandledUser,
NONE_AUTH_SCHEME,
} from '@novu/shared';
import { PinoLogger } from '../../logging';
import { ApiAuthSchemeEnum, IJwtClaims, PassportStrategyEnum, HandledUser, NONE_AUTH_SCHEME } from '@novu/shared';
import { PinoLogger } from '@novu/application-generic';

@Injectable()
export class CommunityUserAuthGuard extends AuthGuard([
PassportStrategyEnum.JWT,
PassportStrategyEnum.HEADER_API_KEY,
]) {
export class CommunityUserAuthGuard extends AuthGuard([PassportStrategyEnum.JWT, PassportStrategyEnum.HEADER_API_KEY]) {
constructor(
private readonly reflector: Reflector,
private readonly logger: PinoLogger,
private readonly logger: PinoLogger
) {
super();
}
Expand All @@ -42,12 +29,8 @@ export class CommunityUserAuthGuard extends AuthGuard([
defaultStrategy: PassportStrategyEnum.JWT,
};
case ApiAuthSchemeEnum.API_KEY: {
const apiEnabled = this.reflector.get<boolean>(
'external_api_accessible',
context.getHandler(),
);
if (!apiEnabled)
throw new UnauthorizedException('API endpoint not available');
const apiEnabled = this.reflector.get<boolean>('external_api_accessible', context.getHandler());
if (!apiEnabled) throw new UnauthorizedException('API endpoint not available');

return {
session: false,
Expand All @@ -57,9 +40,7 @@ export class CommunityUserAuthGuard extends AuthGuard([
case NONE_AUTH_SCHEME:
throw new UnauthorizedException('Missing authorization header');
default:
throw new UnauthorizedException(
`Invalid authentication scheme: "${authScheme}"`,
);
throw new UnauthorizedException(`Invalid authentication scheme: "${authScheme}"`);
}
}

Expand All @@ -68,9 +49,10 @@ export class CommunityUserAuthGuard extends AuthGuard([
user: IJwtClaims | false,
info: any,
context: ExecutionContext,
status?: any,
status?: any
): TUser {
let handledUser: HandledUser;

if (typeof user === 'object') {
/**
* This helps with sentry and other tools that need to know who the user is based on `id` property.
Expand All @@ -79,7 +61,7 @@ export class CommunityUserAuthGuard extends AuthGuard([
...user,
id: user._id,
username: (user.firstName || '').trim(),
domain: user.email?.split('@')[1],
domain: user.email?.split('@')[1] || '',
};
} else {
handledUser = user;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { CanActivate, ExecutionContext, forwardRef, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '@novu/application-generic';
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '../services/auth.service';

@Injectable()
export class RootEnvironmentGuard implements CanActivate {
constructor(@Inject(forwardRef(() => AuthService)) private authService: AuthService) {}
constructor(private authService: AuthService) {}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to use this magic if it lives in the same package


async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
Expand Down
30 changes: 28 additions & 2 deletions apps/api/src/app/auth/framework/user.auth.guard.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
import { UserAuthGuard } from '@novu/application-generic';
import { Injectable, ExecutionContext, Inject } from '@nestjs/common';
import { IAuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';
import { UserSessionData } from '@novu/shared';
import { Instrument } from '@novu/application-generic';

export { UserAuthGuard };
@Injectable()
export class UserAuthGuard {
constructor(@Inject('USER_AUTH_GUARD') private authGuard: IAuthGuard) {}

@Instrument()
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
return this.authGuard.canActivate(context);
}

getAuthenticateOptions(context: ExecutionContext) {
return this.authGuard.getAuthenticateOptions(context);
}

handleRequest<TUser = UserSessionData>(
err: any,
user: UserSessionData | false,
info: any,
context: ExecutionContext,
status?: any
): TUser {
return this.authGuard.handleRequest(err, user, info, context, status);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,10 @@ import {
MemberRepository,
OrganizationRepository,
} from '@novu/dal';
import { PinoLogger } from '../logging';
import {
AnalyticsService,
CommunityAuthService,
CommunityUserAuthGuard,
} from '../services';
import { CreateUser, SwitchOrganization } from '../usecases';

import { PinoLogger, CreateUser, AnalyticsService } from '@novu/application-generic';
import { CommunityUserAuthGuard } from './framework/community.user.auth.guard';
import { SwitchOrganization } from './usecases/switch-organization/switch-organization.usecase';
import { CommunityAuthService } from './services/community.auth.service';
/**
* Injects community auth providers, or providers handling user management (services, repositories, guards ...) into the application.
* This function is closely related to its enterprise counterpart:
Expand All @@ -28,7 +24,7 @@ import { CreateUser, SwitchOrganization } from '../usecases';
export function injectCommunityAuthProviders(
{ repositoriesOnly }: { repositoriesOnly?: boolean } = {
repositoriesOnly: true,
},
}
) {
const userRepositoryProvider = {
provide: 'USER_REPOSITORY',
Expand Down Expand Up @@ -56,7 +52,7 @@ export function injectCommunityAuthProviders(
organizationRepository: OrganizationRepository,
environmentRepository: EnvironmentRepository,
memberRepository: MemberRepository,
switchOrganizationUsecase: SwitchOrganization,
switchOrganizationUsecase: SwitchOrganization
) => {
return new CommunityAuthService(
userRepository,
Expand All @@ -67,7 +63,7 @@ export function injectCommunityAuthProviders(
organizationRepository,
environmentRepository,
memberRepository,
switchOrganizationUsecase,
switchOrganizationUsecase
);
},
inject: [
Expand All @@ -92,11 +88,7 @@ export function injectCommunityAuthProviders(
};

if (repositoriesOnly) {
return [
userRepositoryProvider,
memberRepositoryProvider,
organizationRepositoryProvider,
];
return [userRepositoryProvider, memberRepositoryProvider, organizationRepositoryProvider];
}

return [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { MemberEntity, SubscriberEntity, UserEntity } from '@novu/dal';
import {
AuthProviderEnum,
AuthenticateContext,
ISubscriberJwt,
UserSessionData,
} from '@novu/shared';
import { IAuthService } from './auth.service.interface';
import { AuthProviderEnum, AuthenticateContext, ISubscriberJwt, UserSessionData } from '@novu/shared';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need these to remain in shared?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these likely need to stay e.g. ISubscriberJwt is used in @novu/ws, but I would assume atleast half of the types can probably be also moved to API - we can revise types in other PRs.

import { IAuthService } from '@novu/application-generic';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one interface needs to stay in app-generic because its reused in EE auth.

We need to have common auth interface for private and public authentication so it makes sense.


@Injectable()
export class AuthService implements IAuthService {
Expand All @@ -24,30 +19,17 @@ export class AuthService implements IAuthService {
id: string;
},
distinctId: string,
authContext: AuthenticateContext = {},
authContext: AuthenticateContext = {}
): Promise<{ newUser: boolean; token: string }> {
return this.authService.authenticate(
authProvider,
accessToken,
refreshToken,
profile,
distinctId,
authContext,
);
return this.authService.authenticate(authProvider, accessToken, refreshToken, profile, distinctId, authContext);
}

refreshToken(userId: string): Promise<string> {
return this.authService.refreshToken(userId);
}

isAuthenticatedForOrganization(
userId: string,
organizationId: string,
): Promise<boolean> {
return this.authService.isAuthenticatedForOrganization(
userId,
organizationId,
);
isAuthenticatedForOrganization(userId: string, organizationId: string): Promise<boolean> {
return this.authService.isAuthenticatedForOrganization(userId, organizationId);
}

getUserByApiKey(apiKey: string): Promise<UserSessionData> {
Expand All @@ -66,21 +48,16 @@ export class AuthService implements IAuthService {
user: UserEntity,
organizationId?: string,
member?: MemberEntity,
environmentId?: string,
environmentId?: string
): Promise<string> {
return this.authService.getSignedToken(
user,
organizationId,
member,
environmentId,
);
return this.authService.getSignedToken(user, organizationId, member, environmentId);
}

validateUser(payload: UserSessionData): Promise<UserEntity> {
return this.authService.validateUser(payload);
}

validateSubscriber(payload: ISubscriberJwt): Promise<SubscriberEntity> {
validateSubscriber(payload: ISubscriberJwt): Promise<SubscriberEntity | null> {
return this.authService.validateSubscriber(payload);
}

Expand Down
Loading
Loading