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

added routes for authenticate domain & test logic #57

Merged
merged 5 commits into from
Sep 13, 2024
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
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
Loading