Skip to content

Commit

Permalink
added routes for authenticate domain & test logic (#57)
Browse files Browse the repository at this point in the history
Co-authored-by: Tejas Mehta <[email protected]>
  • Loading branch information
xHayden and tmthecoder committed Sep 13, 2024
1 parent b47ff29 commit 25cdb55
Show file tree
Hide file tree
Showing 21 changed files with 1,145 additions and 852 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
},
"dependencies": {
"@sendgrid/mail": "^8.1.1",
"axios": "^1.6.7",
"bcrypt": "^5.1.1",
"fs-extra": "^11.2.0",
"jsonwebtoken": "^9.0.2"
Expand Down
52 changes: 52 additions & 0 deletions packages/api-gateway/src/middleware/email.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
Injectable,
NestMiddleware,
Inject,
OnModuleInit,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { ClientGrpc } from '@nestjs/microservices';
import { JwtProto } from 'juno-proto';
import { lastValueFrom } from 'rxjs';

const { JWT_SERVICE_NAME } = JwtProto;

@Injectable()
export class EmailLinkingMiddleware implements NestMiddleware, OnModuleInit {
private jwtService: JwtProto.JwtServiceClient;

constructor(@Inject(JWT_SERVICE_NAME) private jwtClient: ClientGrpc) {}

onModuleInit() {
this.jwtService = this.jwtClient.getService<JwtProto.JwtServiceClient>(
JwtProto.JWT_SERVICE_NAME,
);
}

async use(req: Request, res: Response, next: NextFunction) {
try {
if (!req.headers.authorization) {
throw new Error('No authorization headers');
}
const token = this.extractTokenFromHeader(req);
if (!token) {
throw new Error('Jwt not found');
}
const jwtValidation = this.jwtService.validateJwt({ jwt: token });
await lastValueFrom(jwtValidation);
next();
} catch {
throw new HttpException(
'Invalid user credentials',
HttpStatus.UNAUTHORIZED,
);
}
}

private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
22 changes: 21 additions & 1 deletion packages/api-gateway/src/modules/email/email.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,31 @@ export class EmailController implements OnModuleInit {
})
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@ApiBadRequestResponse({ description: 'Bad Request' })
@Post('register')
@Post('/register-sender')
async registerSenderAddress(@Body('') params: RegisterEmailModel) {
return new RegisterEmailResponse(params.email);
}

@Post('/register-domain')
async registerEmailDomain(
@Body('domain') domain: string,
@Body('subdomain') subdomain: string,
): Promise<EmailProto.AuthenticateDomainResponse> {
if (!domain) {
throw new HttpException(
'Cannot register domain (no domain supplied)',
HttpStatus.BAD_REQUEST,
);
}

const res = this.emailService.authenticateDomain({
domain,
subdomain,
});

return lastValueFrom(res);
}

@ApiOperation({
description: 'This endpoint sends an email',
})
Expand Down
4 changes: 2 additions & 2 deletions packages/api-gateway/src/modules/email/email.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { ConfigModule } from '@nestjs/config';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { join } from 'path';
import { EmailController } from './email.controller';
import { ProjectLinkingMiddleware } from 'src/middleware/project.middleware';
import { EmailProto, EmailProtoFile, JwtProto, JwtProtoFile } from 'juno-proto';
import { EmailLinkingMiddleware } from 'src/middleware/email.middleware';

const { JWT_SERVICE_NAME, JUNO_JWT_PACKAGE_NAME } = JwtProto;
const { EMAIL_SERVICE_NAME, JUNO_EMAIL_PACKAGE_NAME } = EmailProto;
Expand Down Expand Up @@ -39,6 +39,6 @@ const { EMAIL_SERVICE_NAME, JUNO_EMAIL_PACKAGE_NAME } = EmailProto;
})
export class EmailModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(ProjectLinkingMiddleware).forRoutes('email/*');
consumer.apply(EmailLinkingMiddleware).forRoutes('email/*');
}
}
68 changes: 48 additions & 20 deletions packages/api-gateway/test/email.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ describe('Email Registration Routes', () => {
it('Registers an email without a body', () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/register')
.post('/email/register-sender')
.set('Authorization', 'Bearer ' + token)
.expect(400);
});
it('Has been called with a malformed emaiil', () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/register')
.post('/email/register-sender')
.set('Authorization', 'Bearer ' + token)
.send({
email: 'invalidemail', // Malformed email
Expand All @@ -69,15 +69,15 @@ describe('Email Registration Routes', () => {
});
it('Registration endpoint called with no Authorization header', () => {
return request(app.getHttpServer())
.post('/email/register')
.post('/email/register-sender')
.send({
email: '[email protected]',
})
.expect(401);
});
it('Registration endpoint called with an invalid JWT', () => {
return request(app.getHttpServer())
.post('/email/register')
.post('/email/register-sender')
.set('Authorization', 'Bearer invalid.jwt.token')
.send({
email: '[email protected]',
Expand All @@ -88,7 +88,7 @@ describe('Email Registration Routes', () => {
// Assuming 'valid.jwt.token' is a placeholder for a valid JWT obtained in a way relevant to your test setup
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/register')
.post('/email/register-sender')
.set('Authorization', 'Bearer ' + token)
.send({
email: '[email protected]',
Expand All @@ -98,21 +98,6 @@ describe('Email Registration Routes', () => {
});

describe('Email Sending Route', () => {
let app: INestApplication;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();
});

afterAll(async () => {
await app.close();
});

it('should return 401 when Authorization header is missing', async () => {
return request(app.getHttpServer())
.post('/email/send')
Expand Down Expand Up @@ -398,3 +383,46 @@ describe('Email Sending Route', () => {
.expect(400);
});
});

describe('Domain Registration Routes', () => {
it('Registers a domain without a domain parameter', () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/register-domain')
.set('Authorization', 'Bearer ' + token)
.expect(400);
});

it('Registers a domain with valid parameters', () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/register-domain')
.set('Authorization', 'Bearer ' + token)
.send({
domain: 'example.com',
subdomain: 'sub',
})
.expect(201);
});

it('Registration endpoint called with no Authorization header', () => {
return request(app.getHttpServer())
.post('/email/register-domain')
.send({
domain: 'example.com',
subdomain: 'sub',
})
.expect(401);
});

it('Registration endpoint called with an invalid JWT', () => {
return request(app.getHttpServer())
.post('/email/register-domain')
.set('Authorization', 'Bearer invalid.jwt.token')
.send({
domain: 'example.com',
subdomain: 'sub',
})
.expect(401);
});
});
2 changes: 2 additions & 0 deletions packages/email-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
"@nestjs/core": "^10.0.0",
"@nestjs/microservices": "^10.2.7",
"@nestjs/platform-express": "^10.0.0",
"@sendgrid/mail": "^8.1.3",
"@sentry/nestjs": "^8.29.0",
"@sentry/profiling-node": "^8.29.0",
"axios": "^1.7.7",
"juno-proto": "file:../proto",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
Expand Down
1 change: 0 additions & 1 deletion packages/email-service/src/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {}
9 changes: 8 additions & 1 deletion packages/email-service/src/modules/email/email.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller } from '@nestjs/common';
import { Body, Controller } from '@nestjs/common';
import { EmailService } from './email.service';
import { EmailProto } from 'juno-proto';
import { RpcException } from '@nestjs/microservices';
Expand All @@ -9,6 +9,12 @@ import { status } from '@grpc/grpc-js';
export class EmailController implements EmailProto.EmailServiceController {
constructor(private readonly emailService: EmailService) {}

async authenticateDomain(
@Body() req: EmailProto.AuthenticateDomainRequest,
): Promise<EmailProto.AuthenticateDomainResponse> {
return await this.emailService.authenticateDomain(req);
}

async sendEmail(
request: EmailProto.SendEmailRequest,
): Promise<EmailProto.SendEmailResponse> {
Expand Down Expand Up @@ -57,6 +63,7 @@ export class EmailController implements EmailProto.EmailServiceController {
throw new RpcException(error.message);
}
}

async registerSender(
req: EmailProto.RegisterSenderRequest,
): Promise<EmailProto.RegisterSenderResponse> {
Expand Down
74 changes: 74 additions & 0 deletions packages/email-service/src/modules/email/email.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,80 @@ import { RpcException } from '@nestjs/microservices';
export class EmailService {
constructor(private sendgrid: SendGridService) {}

async authenticateDomain(
req: EmailProto.AuthenticateDomainRequest,
): Promise<EmailProto.AuthenticateDomainResponse> {
if (!req.domain || req.domain.length == 0) {
throw new RpcException('Cannot register domain (no domain supplied)');
}

const sendGridApiKey = process.env.SENDGRID_API_KEY;

if (!sendGridApiKey) {
throw new RpcException(
'Cannot register domain (SendGrid API key not in .env)',
);
}

const sendGridUrl = 'https://api.sendgrid.com/v3/whitelabel/domains';

if (process.env['NODE_ENV'] == 'test') {
return {
statusCode: 201,
id: 0,
valid: 'true',
records: {
mailCname: {
valid: true,
type: 'cname',
host: 'mail',
data: 'mail.sendgrid.net',
},
dkim1: {
valid: true,
type: 'cname',
host: 's1._domainkey',
data: 's1.domainkey.u1234.wl.sendgrid.net',
},
dkim2: {
valid: true,
type: 'cname',
host: 's2._domainkey',
data: 's2.domainkey.u1234.wl.sendgrid.net',
},
},
};
}

try {
const response = await axios.post(
sendGridUrl,
{
domain: req.domain,
subdomain: req.subdomain,
},
{
headers: {
Authorization: `Bearer ${sendGridApiKey}`,
'Content-Type': 'application/json',
},
},
);

const records: EmailProto.SendGridDnsRecords = response.data.dns;

return {
statusCode: response.status,
id: response.data.id,
valid: response.data.valid,
records,
};
} catch (error) {
console.error('Error registering domain:', error);
throw new RpcException('Failed to register domain');
}
}

async sendEmail(request: EmailProto.SendEmailRequest): Promise<void> {
// SendGrid Client for future integration with API
// Conditional statement used for testing without actually calling Sendgrid. Remove when perform actual integration
Expand Down
Loading

0 comments on commit 25cdb55

Please sign in to comment.