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

Use Brevo Rest API #218

Merged
merged 7 commits into from
Aug 6, 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
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.")]
TheTedder marked this conversation as resolved.
Show resolved Hide resolved
[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")]
TheTedder marked this conversation as resolved.
Show resolved Hide resolved
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