Skip to content

Commit

Permalink
Use Brevo Rest API (leaderboardsgg#218)
Browse files Browse the repository at this point in the history
* Add brevo csharp client.

* Add Brevo options.

* Add Brevo service.

* Replace EmailSender with BrevoService.

* Update TestApiFactory to mock Brevo.

* Mark EmailSender as Obsolete.

* formatting
  • Loading branch information
TheTedder authored Aug 6, 2024
1 parent be5d650 commit 3b6433c
Show file tree
Hide file tree
Showing 9 changed files with 64 additions and 37 deletions.
2 changes: 2 additions & 0 deletions LeaderboardBackend.Test/Features/Emails/EmailSenderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

namespace LeaderboardBackend.Test.Features.Emails;

[Obsolete("EmailSender is no longer used.")]
[Ignore("EmailSender is no longer used.")]
public class EmailSenderTests
{
private IEmailSender _sut = null!;
Expand Down
5 changes: 2 additions & 3 deletions LeaderboardBackend.Test/TestApi/TestApiFactory.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System;
using System.Net.Http;
using LeaderboardBackend.Models.Entities;
using LeaderboardBackend.Services;
using LeaderboardBackend.Test.Lib;
using MailKit.Net.Smtp;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -41,8 +41,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
};
});

// mock SMTP client
services.Replace(ServiceDescriptor.Transient<ISmtpClient>(_ => new Mock<ISmtpClient>().Object));
services.Replace(ServiceDescriptor.Singleton(_ => new Mock<IEmailSender>().Object));

using IServiceScope scope = services.BuildServiceProvider().CreateScope();
ApplicationContext dbContext =
Expand Down
1 change: 1 addition & 0 deletions LeaderboardBackend/LeaderboardBackend.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="brevo_csharp" Version="1.0.0" />
<PackageReference Include="DotNetEnv" Version="3.0.0" />
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
<PackageReference Include="FluentValidation" Version="11.9.2" />
Expand Down
43 changes: 16 additions & 27 deletions LeaderboardBackend/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
using LeaderboardBackend.Models.Entities;
using LeaderboardBackend.Services;
using LeaderboardBackend.Swagger;
using MailKit.Net.Smtp;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -34,7 +33,7 @@
{
AppConfig? appConfigWithoutDotEnv = builder.Configuration.Get<AppConfig>();
EnvConfigurationSource dotEnvSource =
new(new string[] { appConfigWithoutDotEnv?.EnvPath ?? ".env" }, LoadOptions.NoClobber());
new([appConfigWithoutDotEnv?.EnvPath ?? ".env"], LoadOptions.NoClobber());
builder.Configuration.Sources.Insert(0, dotEnvSource); // all other configuration providers override .env
}

Expand All @@ -48,8 +47,8 @@
.ValidateOnStart();

builder.Services
.AddOptions<EmailSenderConfig>()
.BindConfiguration(EmailSenderConfig.KEY)
.AddOptions<BrevoOptions>()
.BindConfiguration(BrevoOptions.KEY)
.ValidateFluentValidation()
.ValidateOnStart();

Expand Down Expand Up @@ -104,33 +103,26 @@
builder.Services.AddScoped<IAccountConfirmationService, AccountConfirmationService>();
builder.Services.AddScoped<IAccountRecoveryService, AccountRecoveryService>();
builder.Services.AddScoped<IRunService, RunService>();
builder.Services.AddSingleton<IEmailSender, EmailSender>();
builder.Services.AddSingleton<ISmtpClient>(_ => new SmtpClient() { Timeout = 3000 });
builder.Services.AddSingleton<IEmailSender, BrevoService>();
builder.Services.AddSingleton<IClock>(_ => SystemClock.Instance);

AppConfig? appConfig = builder.Configuration.Get<AppConfig>();
if (!string.IsNullOrWhiteSpace(appConfig?.AllowedOrigins))
{
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(
builder.Services.AddCors(options => options.AddDefaultPolicy(
policy =>
policy
.WithOrigins(appConfig.ParseAllowedOrigins())
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowAnyMethod()
.AllowAnyHeader()
);
});
));
}
else if (builder.Environment.IsDevelopment())
{
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(
builder.Services.AddCors(options => options.AddDefaultPolicy(
policy => policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()
);
});
));
}

// Add controllers to the container.
Expand Down Expand Up @@ -237,19 +229,14 @@
}
)
.SetDefaultPolicy(new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(new[] { JwtBearerDefaults.AuthenticationScheme })
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.AddRequirements(new[] { new UserTypeRequirement(UserTypes.USER) })
.Build());

builder.Services.AddSingleton<IValidatorInterceptor, LeaderboardBackend.Models.Validation.UseErrorCodeInterceptor>();
builder.Services.AddFluentValidationAutoValidation(c =>
{
c.DisableDataAnnotationsValidation = true;
});
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context =>
builder.Services.AddFluentValidationAutoValidation(c => c.DisableDataAnnotationsValidation = true);
builder.Services.Configure<ApiBehaviorOptions>(options => options.InvalidModelStateResponseFactory = context =>
{
ValidationProblemDetails problemDetails = new(context.ModelState);

Expand All @@ -258,14 +245,13 @@
// For JSON syntax errors that we don't override, their keys will be the field
// path that has the error, which always starts with "$", denoting the object
// root. We check for that, and return the error code accordingly. - zysim
if (problemDetails.Errors.Keys.Any(x => x.StartsWith("$")))
if (problemDetails.Errors.Keys.Any(x => x.StartsWith('$')))
{
return new BadRequestObjectResult(problemDetails);
}

return new UnprocessableEntityObjectResult(problemDetails);
};
});
});

// Can't use AddSingleton here since we call the DB in the Handler
builder.Services.AddScoped<IAuthorizationHandler, UserTypeAuthorizationHandler>();
Expand All @@ -278,6 +264,9 @@
#region WebApplication
WebApplication app = builder.Build();

BrevoOptions brevoOptions = app.Services.GetRequiredService<IOptionsMonitor<BrevoOptions>>().CurrentValue;
brevo_csharp.Client.Configuration.Default.AddApiKey("api-key", brevoOptions.ApiKey);

// Configure the HTTP request pipeline.
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "LeaderboardBackend v1"));
Expand Down
20 changes: 20 additions & 0 deletions LeaderboardBackend/Services/Impl/BrevoOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using FluentValidation;

namespace LeaderboardBackend.Services;

public class BrevoOptions
{
public const string KEY = "Brevo";
public string ApiKey { get; set; } = string.Empty;
public required string SenderName { get; set; }
public required string SenderEmail { get; set; }
}

public class BrevoOptionsValidator : AbstractValidator<BrevoOptions>
{
public BrevoOptionsValidator()
{
RuleFor(x => x.SenderName).NotEmpty();
RuleFor(x => x.SenderEmail).EmailAddress();
}
}
18 changes: 18 additions & 0 deletions LeaderboardBackend/Services/Impl/BrevoService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using brevo_csharp.Api;
using brevo_csharp.Model;
using Microsoft.Extensions.Options;

namespace LeaderboardBackend.Services;

public class BrevoService(IOptions<BrevoOptions> options, ILogger<BrevoService> logger) : IEmailSender
{
private readonly TransactionalEmailsApi _transactionalEmailsApi = new();
private readonly SendSmtpEmailSender _smtpEmailSender = new(options.Value.SenderName, options.Value.SenderEmail);

public async System.Threading.Tasks.Task EnqueueEmailAsync(string recipientAddress, string subject, string htmlMessage)
{
SendSmtpEmail email = new(_smtpEmailSender, [new(recipientAddress)], null, null, htmlMessage, null, subject);
CreateSmtpEmail result = await _transactionalEmailsApi.SendTransacEmailAsync(email);
logger.LogInformation("Email sent with id {Id}", result.MessageId);
}
}
1 change: 1 addition & 0 deletions LeaderboardBackend/Services/Impl/EmailSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace LeaderboardBackend.Services;

[Obsolete("Replaced by BrevoService")]
public class EmailSender : IEmailSender
{
private readonly EmailSenderConfig _config;
Expand Down
4 changes: 2 additions & 2 deletions LeaderboardBackend/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
},
"EnvPath": ".env",
"AllowedHosts": "*",
"EmailSender": {
"Brevo": {
"SenderName": "Leaderboards.gg",
"SenderAddress": "[email protected]"
"SenderEmail": "[email protected]"
},
"Feature": {
"AccountRecovery": true,
Expand Down
7 changes: 2 additions & 5 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ ApplicationContext__PG__PORT=5432
JWT__KEY=coolsecretkeywowitssoincredibleiloveit
JWT__ISSUER=leaderboards.gg

EmailSender__Smtp__Host=smtp-relay.sendinblue.com
EmailSender__Smtp__Port=587
EmailSender__Smtp__Username=User42
EmailSender__Smtp__Password=Password123

ADMINER_PORT=1337

Brevo__ApiKey=my-brevo-api-key

0 comments on commit 3b6433c

Please sign in to comment.