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

Nv 2781 move auth logic to application generic #4064

Merged
Show file tree
Hide file tree
Changes from all commits
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
58 changes: 9 additions & 49 deletions apps/api/src/app/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,13 @@ import { MemberRepository, OrganizationRepository, UserRepository, MemberEntity
import { JwtService } from '@nestjs/jwt';
import { AuthGuard } from '@nestjs/passport';
import { IJwtPayload } from '@novu/shared';
import { AuthService } from './services/auth.service';
import { UserRegistrationBodyDto } from './dtos/user-registration.dto';
import { UserRegister } from './usecases/register/user-register.usecase';
import { UserRegisterCommand } from './usecases/register/user-register.command';
import { Login } from './usecases/login/login.usecase';
import { LoginBodyDto } from './dtos/login.dto';
import { LoginCommand } from './usecases/login/login.command';
import { UserSession } from '../shared/framework/user.decorator';
import { SwitchEnvironment } from './usecases/switch-environment/switch-environment.usecase';
import { SwitchEnvironmentCommand } from './usecases/switch-environment/switch-environment.command';
import { SwitchOrganization } from './usecases/switch-organization/switch-organization.usecase';
import { SwitchOrganizationCommand } from './usecases/switch-organization/switch-organization.command';
import { JwtAuthGuard } from './framework/auth.guard';
import { PasswordResetRequestCommand } from './usecases/password-reset-request/password-reset-request.command';
import { PasswordResetRequest } from './usecases/password-reset-request/password-reset-request.usecase';
Expand All @@ -40,6 +35,14 @@ import { PasswordReset } from './usecases/password-reset/password-reset.usecase'
import { ApiException } from '../shared/exceptions/api.exception';
import { ApiExcludeController, ApiTags } from '@nestjs/swagger';
import { PasswordResetBodyDto } from './dtos/password-reset.dto';
import {
AuthService,
buildOauthRedirectUrl,
SwitchEnvironment,
SwitchEnvironmentCommand,
SwitchOrganization,
SwitchOrganizationCommand,
} from '@novu/application-generic';

@Controller('/auth')
@UseInterceptors(ClassSerializerInterceptor)
Expand Down Expand Up @@ -80,50 +83,7 @@ export class AuthController {
@Get('/github/callback')
@UseGuards(AuthGuard('github'))
async githubCallback(@Req() request, @Res() response) {
if (!request.user || !request.user.token) {
return response.redirect(`${process.env.FRONT_BASE_URL + '/auth/login'}?error=AuthenticationError`);
}

let url = process.env.FRONT_BASE_URL + '/auth/login';
const redirectUrl = JSON.parse(request.query.state).redirectUrl;

/**
* Make sure we only allow localhost redirects for CLI use and our own success route
* https://github.com/novuhq/novu/security/code-scanning/3
*/
if (redirectUrl && redirectUrl.startsWith('http://localhost:') && !redirectUrl.includes('@')) {
url = redirectUrl;
}

url += `?token=${request.user.token}`;

if (request.user.newUser) {
url += '&newUser=true';
}

/**
* partnerCode, next and configurationId are required during external partners integration
* such as vercel integration etc
*/
const partnerCode = JSON.parse(request.query.state).partnerCode;
if (partnerCode) {
url += `&code=${partnerCode}`;
}

const next = JSON.parse(request.query.state).next;
if (next) {
url += `&next=${next}`;
}

const configurationId = JSON.parse(request.query.state).configurationId;
if (configurationId) {
url += `&configurationId=${configurationId}`;
}

const invitationToken = JSON.parse(request.query.state).invitationToken;
if (invitationToken) {
url += `&invitationToken=${invitationToken}`;
}
const url = buildOauthRedirectUrl(request);

return response.redirect(url);
}
Expand Down
9 changes: 6 additions & 3 deletions apps/api/src/app/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { MiddlewareConsumer, Module, NestModule, Provider, RequestMethod } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule, PassportStrategy } from '@nestjs/passport';
import { PassportModule } from '@nestjs/passport';
import * as passport from 'passport';

import { AuthProviderEnum } from '@novu/shared';
import { AuthService } from '@novu/application-generic';

import { RolesGuard } from './framework/roles.guard';
import { JwtStrategy } from './services/passport/jwt.strategy';
import { AuthController } from './auth.controller';
import { UserModule } from '../user/user.module';
import { AuthService } from './services/auth.service';
import { USE_CASES } from './usecases';
import { SharedModule } from '../shared/shared.module';
import { GitHubStrategy } from './services/passport/github.strategy';
Expand Down Expand Up @@ -56,7 +59,7 @@ export class AuthModule implements NestModule {
if (process.env.GITHUB_OAUTH_CLIENT_ID) {
consumer
.apply(
passport.authenticate('github', {
passport.authenticate(AuthProviderEnum.GITHUB, {
session: false,
scope: ['user:email'],
})
Expand Down
3 changes: 1 addition & 2 deletions apps/api/src/app/auth/framework/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ExecutionContext, forwardRef, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { AuthService } from '../services/auth.service';
import { Instrument, PinoLogger } from '@novu/application-generic';
import { AuthService, Instrument, PinoLogger } from '@novu/application-generic';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { CanActivate, ExecutionContext, forwardRef, Inject, Injectable, Unauthor
import { Reflector } from '@nestjs/core';
import { IJwtPayload } from '@novu/shared';
import * as jwt from 'jsonwebtoken';
import { AuthService } from '../services/auth.service';
import { AuthService } from '@novu/application-generic';

@Injectable()
export class RootEnvironmentGuard implements CanActivate {
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/app/auth/services/passport/github.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { PassportStrategy } from '@nestjs/passport';
import * as githubPassport from 'passport-github2';
import { Metadata, StateStoreStoreCallback, StateStoreVerifyCallback } from 'passport-oauth2';
import { AuthProviderEnum } from '@novu/shared';
import { AuthService } from '../auth.service';
import { AuthService } from '@novu/application-generic';

@Injectable()
export class GitHubStrategy extends PassportStrategy(githubPassport.Strategy, 'github') {
export class GitHubStrategy extends PassportStrategy(githubPassport.Strategy, AuthProviderEnum.GITHUB) {
constructor(private authService: AuthService) {
super({
clientID: process.env.GITHUB_OAUTH_CLIENT_ID,
Expand Down
3 changes: 1 addition & 2 deletions apps/api/src/app/auth/services/passport/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { IJwtPayload } from '@novu/shared';
import { AuthService } from '../auth.service';
import { Instrument, PinoLogger } from '@novu/application-generic';
import { AuthService, Instrument, PinoLogger } from '@novu/application-generic';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ISubscriberJwt } from '@novu/shared';
import { AuthService } from '../auth.service';
import { AuthService } from '@novu/application-generic';

@Injectable()
export class JwtSubscriberStrategy extends PassportStrategy(Strategy, 'subscriberJwt') {
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/app/auth/usecases/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { SwitchEnvironment, SwitchOrganization } from '@novu/application-generic';

import { PasswordResetRequest } from './password-reset-request/password-reset-request.usecase';
import { UserRegister } from './register/user-register.usecase';
import { Login } from './login/login.usecase';
import { SwitchEnvironment } from './switch-environment/switch-environment.usecase';
import { SwitchOrganization } from './switch-organization/switch-organization.usecase';
import { PasswordReset } from './password-reset/password-reset.usecase';

export const USE_CASES = [
Expand Down
3 changes: 1 addition & 2 deletions apps/api/src/app/auth/usecases/login/login.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import * as bcrypt from 'bcrypt';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { differenceInMinutes, parseISO } from 'date-fns';
import { UserRepository, UserEntity, OrganizationRepository } from '@novu/dal';
import { AnalyticsService } from '@novu/application-generic';
import { AnalyticsService, AuthService } from '@novu/application-generic';

import { LoginCommand } from './login.command';
import { ApiException } from '../../../shared/exceptions/api.exception';
import { normalizeEmail } from '../../../shared/helpers/email-normalization.service';
import { AuthService } from '../../services/auth.service';
import { createHash } from '../../../shared/helpers/hmac.service';

@Injectable()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
import { isBefore, subDays } from 'date-fns';
import { UserRepository } from '@novu/dal';
import { buildUserKey, InvalidateCacheService } from '@novu/application-generic';
import { AuthService, buildUserKey, InvalidateCacheService } from '@novu/application-generic';

import { PasswordResetCommand } from './password-reset.command';
import { ApiException } from '../../../shared/exceptions/api.exception';
import { AuthService } from '../../services/auth.service';

@Injectable()
export class PasswordReset {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import { Inject, Injectable } from '@nestjs/common';
import { OrganizationEntity, UserRepository } from '@novu/dal';
import * as bcrypt from 'bcrypt';
import { SignUpOriginEnum } from '@novu/shared';
import { AnalyticsService } from '@novu/application-generic';
import { AnalyticsService, AuthService } from '@novu/application-generic';

import { AuthService } from '../../services/auth.service';
import { UserRegisterCommand } from './user-register.command';
import { normalizeEmail } from '../../../shared/helpers/email-normalization.service';
import { ApiException } from '../../../shared/exceptions/api.exception';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';

import { MemberEntity, OrganizationRepository, UserEntity, MemberRepository, UserRepository } from '@novu/dal';
import { MemberStatusEnum } from '@novu/shared';
import { Novu } from '@novu/node';
import { AuthService } from '@novu/application-generic';

import { ApiException } from '../../../shared/exceptions/api.exception';
import { AcceptInviteCommand } from './accept-invite.command';
import { AuthService } from '../../../auth/services/auth.service';
import { capitalize } from '../../../shared/services/helper/helper.service';

@Injectable({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Injectable } from '@nestjs/common';
import { faker } from '@faker-js/faker';

import { AuthService } from '@novu/application-generic';

import { SeedDataCommand } from './seed-data.command';
import { AuthService } from '../../../auth/services/auth.service';
import { UserRegister } from '../../../auth/usecases/register/user-register.usecase';
import { UserRegisterCommand } from '../../../auth/usecases/register/user-register.command';
import { ApiException } from '../../../shared/exceptions/api.exception';
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/app/user/usecases/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CreateUser } from './create-user/create-user.usecase';
import { CreateUser } from '@novu/application-generic';

import { GetMyProfileUsecase } from './get-my-profile/get-my-profile.usecase';
import { UpdateOnBoardingUsecase } from './update-on-boarding/update-on-boarding.usecase';
import { UpdateOnBoardingTourUsecase } from './update-on-boarding-tour/update-on-boarding-tour.usecase';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
CreateSubscriberCommand,
SelectIntegrationCommand,
SelectIntegration,
AuthService,
} from '@novu/application-generic';

import { AuthService } from '../../../auth/services/auth.service';
import { ApiException } from '../../../shared/exceptions/api.exception';
import { InitializeSessionCommand } from './initialize-session.command';

Expand Down
1 change: 1 addition & 0 deletions apps/web/src/design-system/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,4 @@ export { DisconnectGradient } from './gradient/DisconnectGradient';
export { BoltOutlinedGradient } from './gradient/BoltOutlinedGradient';

export { GitHub } from './social/GitHub';
export { Google } from './social/Google';
24 changes: 24 additions & 0 deletions apps/web/src/design-system/icons/social/Google.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
/* eslint-disable */
export function Google(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20" fill="none" {...props}>
<path
d="M11.2083 8.18184V12.0546H16.5901C16.3538 13.3001 15.6446 14.3546 14.581 15.0637L17.8265 17.5819C19.7174 15.8365 20.8083 13.2729 20.8083 10.2274C20.8083 9.51832 20.7447 8.83643 20.6265 8.18195L11.2083 8.18184Z"
fill="#4285F4"
/>
<path
d="M5.60401 11.9034L4.87203 12.4637L2.28107 14.4819C3.92653 17.7455 7.29902 20.0001 11.2081 20.0001C13.9081 20.0001 16.1719 19.1092 17.8265 17.5819L14.581 15.0637C13.6901 15.6637 12.5535 16.0274 11.2081 16.0274C8.60811 16.0274 6.39907 14.2729 5.6081 11.9092L5.60401 11.9034Z"
fill="#34A853"
/>
<path
d="M2.28108 5.51822C1.59929 6.86362 1.20842 8.38183 1.20842 10C1.20842 11.6182 1.59928 13.1365 2.28107 14.4819C2.28107 14.4909 5.60843 11.8999 5.60843 11.8999C5.40843 11.2999 5.29021 10.6636 5.29021 9.99989C5.29021 9.33617 5.40843 8.69984 5.60843 8.09984L2.28108 5.51822Z"
fill="#FBBC05"
/>
<path
d="M11.2083 3.98184C12.6811 3.98184 13.9901 4.49092 15.0356 5.47276L17.8992 2.60914C16.1628 0.990977 13.9084 0 11.2083 0C7.29923 0 3.92654 2.24546 2.28108 5.51822L5.60843 8.09984C6.39929 5.73616 8.60831 3.98184 11.2083 3.98184Z"
fill="#EA4335"
/>
</svg>
);
}
56 changes: 39 additions & 17 deletions apps/web/src/pages/auth/components/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import { Divider, Button as MantineButton, Center } from '@mantine/core';
import { useAuthContext } from '../../../components/providers/AuthProvider';
import { api } from '../../../api/api.client';
import { PasswordInput, Button, colors, Input, Text } from '../../../design-system';
import { GitHub } from '../../../design-system/icons';
import { GitHub, Google } from '../../../design-system/icons';
import { IS_DOCKER_HOSTED } from '../../../config';
import { useVercelParams } from '../../../hooks';
import { useAcceptInvite } from './useAcceptInvite';
import { buildGithubLink, buildVercelGithubLink } from './gitHubUtils';
import { buildGithubLink, buildGoogleLink, buildVercelGithubLink } from './gitHubUtils';
import { ROUTES } from '../../../constants/routes.enum';
import { When } from '../../../components/utils/When';

type LoginFormProps = {
invitationToken?: string;
Expand All @@ -41,6 +42,7 @@ export function LoginForm({ email, invitationToken }: LoginFormProps) {
const githubLink = isFromVercel
? buildVercelGithubLink({ code, next, configurationId })
: buildGithubLink({ invitationToken });
const googleLink = buildGoogleLink({ invitationToken });

const {
register,
Expand Down Expand Up @@ -95,24 +97,39 @@ export function LoginForm({ email, invitationToken }: LoginFormProps) {

return (
<>
{!IS_DOCKER_HOSTED && (
<When truthy={!IS_DOCKER_HOSTED}>
<>
<GitHubButton
component="a"
href={githubLink}
my={30}
variant="white"
fullWidth
radius="md"
leftIcon={<GitHub />}
sx={{ color: colors.B40, fontSize: '16px', fontWeight: 700, height: '50px' }}
data-test-id="github-button"
>
Sign In with GitHub
</GitHubButton>
<OAuth>
<GitHubButton
component="a"
href={githubLink}
my={30}
variant="white"
fullWidth
radius="md"
leftIcon={<GitHub />}
sx={{ color: colors.B40, fontSize: '16px', fontWeight: 700, height: '50px', marginRight: 10 }}
data-test-id="github-button"
>
Sign In with GitHub
</GitHubButton>
<GitHubButton
component="a"
href={googleLink}
my={30}
variant="white"
fullWidth
radius="md"
leftIcon={<Google />}
data-test-id="google-button"
sx={{ color: colors.B40, fontSize: '16px', fontWeight: 700, height: '50px', marginLeft: 10 }}
>
Sign In with Google
</GitHubButton>
</OAuth>
<Divider label={<Text color={colors.B40}>Or</Text>} color={colors.B30} labelPosition="center" my="md" />
</>
)}
</When>
<form noValidate onSubmit={handleSubmit(onLogin)}>
<Input
error={errors.email?.message || emailServerError}
Expand Down Expand Up @@ -174,6 +191,11 @@ export function LoginForm({ email, invitationToken }: LoginFormProps) {
);
}

const OAuth = styled.div`
display: flex;
justify-content: space-between;
`;

const GitHubButton = styled(MantineButton)<{
component: 'a';
my: number;
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/pages/auth/components/gitHubUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ export const buildGithubLink = ({ invitationToken }: { invitationToken?: string
return `${API_ROOT}/v1/auth/github?${queryParams.toString()}`;
};

export const buildGoogleLink = ({ invitationToken }: { invitationToken?: string }) => {
const queryParams = new URLSearchParams();
queryParams.append('source', SignUpOriginEnum.WEB);
if (invitationToken) {
queryParams.append('invitationToken', invitationToken);
}

return `${API_ROOT}/v1/auth/google?${queryParams.toString()}`;
};

export const buildVercelGithubLink = ({
code,
next,
Expand Down
Loading