Skip to content

Commit 8f4113e

Browse files
FIN-76 & FIN-77 (#24)
* FIN-76 adding localization support * FIN-76 adjusting tenant localization columns * FIN-76 added endpoint to get tenant and setting culture via header too * FIN-77 added email template resorces and added reset password email in 3 supportted langs (en, es, pt) * FIN-77 wip adding more email template translations * FIN-77 changed all templates to resources * FIN-76 adjusting existing tests * FIN-76 adjusted tests * FIN-76 adjusting mail kit client * FIN-76 adjusting resources * FIN-76 * FIN-76 .. * FIN-76 ... * FIN-76 .... --------- Co-authored-by: Rafael Kaua Dos Santos Chicovis <rafael.chicovis@gestran.com.br>
1 parent cb3a550 commit 8f4113e

44 files changed

Lines changed: 4423 additions & 994 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Fin-Backend.sln.DotSettings.user

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,22 @@
99
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AModelBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd83f823935e343bfaf4ef4c6263b8a12291438_003F33_003Fc1982b24_003FModelBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
1010
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANullable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F6728f66657329080f0f419df519283a455cdca7d2617a05b18553b02da52fa_003FNullable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
1111
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParameterExpression_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F6182caf029c8666b96a8b3bae244ef29b988c59b58e9b3439b794a4f8195bec_003FParameterExpression_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
12+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResourceManager_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ff3a9578133d9b1466b4e4f127186111bf44ba3d0784e9c9c26b41ccfdfb_003FResourceManager_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
1213
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARuntimeType_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbaf2eef7c7bbea3742b74ee71fe7168c3a8c2269a1ce22d51333175656840_003FRuntimeType_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
1314
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AType_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F7e333a9f3297ba553cccfd3b7c3f1f96125b23d09f883e4d6e66d531559a4c_003FType_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
1415
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValidationResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6e670ad021647bd9cd5d3c28cc553172c800_003F99_003F09c94557_003FValidationResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
15-
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=5f989a48_002Defa4_002D493c_002D924b_002D2fab4b9713fd/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
16+
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=5f989a48_002Defa4_002D493c_002D924b_002D2fab4b9713fd/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
1617
&lt;Solution /&gt;
1718
&lt;/SessionState&gt;</s:String>
18-
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=6d201e39_002D0766_002D4794_002Dab10_002D09c6f9f8b4fa/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="MailSenderClientTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
19+
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=6d201e39_002D0766_002D4794_002Dab10_002D09c6f9f8b4fa/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="MailSenderClientTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
1920
&lt;Project Location="/home/rafaelchicovis/git/fin-backend/Fin.Test" Presentation="&amp;lt;Fin.Test&amp;gt;" /&gt;
2021
&lt;/SessionState&gt;</s:String>
22+
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=Fin_002EApplication_002FResources_002FEmailTemplates_002FEmailTemplates/@EntryIndexedValue">True</s:Boolean>
23+
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=Fin_002EApplication_002FResources_002FEmailTemplates_002FResources_002Ees_002DEN/@EntryIndexedValue">False</s:Boolean>
24+
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=Fin_002EApplication_002FResources_002FEmailTemplates_002FResources_002Ees_002DEN/@EntryIndexRemoved">True</s:Boolean>
25+
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=Fin_002EApplication_002FResources_002FEmailTemplates_002FResources/@EntryIndexedValue">False</s:Boolean>
26+
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=Fin_002EApplication_002FResources_002FEmailTemplates_002FResources/@EntryIndexRemoved">True</s:Boolean>
27+
<s:Boolean x:Key="/Default/ResxEditorPersonal/Initialized/@EntryValue">True</s:Boolean>
2128

2229

2330

Fin.Api/Fin.Api.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<Project Sdk="Microsoft.NET.Sdk.Web">
2-
2+
33
<PropertyGroup>
44
<TargetFramework>net9.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
6+
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
67
</PropertyGroup>
7-
8+
89
<ItemGroup>
910
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="9.0.4" />
1011
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Fin.Application.Tenants;
2+
using Fin.Application.Tenants.Dtos;
3+
using Microsoft.AspNetCore.Authorization;
4+
using Microsoft.AspNetCore.Mvc;
5+
6+
namespace Fin.Api.Tenants;
7+
8+
[Route("tenants")]
9+
[Authorize]
10+
public class TenantController(ITenantService service): ControllerBase
11+
{
12+
[HttpGet("{id:guid}")]
13+
public async Task<ActionResult<TenantOutput>> Get([FromRoute] Guid id)
14+
{
15+
var menu = await service.Get(id);
16+
return menu != null ? Ok(menu) : NotFound();
17+
}
18+
}

Fin.Application/Authentications/Services/AuthenticationService.cs

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
using Fin.Application.Authentications.Dtos;
22
using Fin.Application.Authentications.Enums;
3-
using Fin.Application.Authentications.Utils;
3+
using Fin.Application.Emails;
44
using Fin.Application.Globals.Dtos;
55
using Fin.Application.Users.Services;
66
using Fin.Domain.Global;
7-
using Fin.Domain.Tenants.Entities;
87
using Fin.Domain.Users.Dtos;
98
using Fin.Domain.Users.Entities;
109
using Fin.Infrastructure.Authentications;
@@ -14,7 +13,6 @@
1413
using Fin.Infrastructure.AutoServices.Interfaces;
1514
using Fin.Infrastructure.Constants;
1615
using Fin.Infrastructure.Database.Repositories;
17-
using Fin.Infrastructure.EmailSenders;
1816
using Fin.Infrastructure.EmailSenders.Dto;
1917
using Fin.Infrastructure.Redis;
2018
using Microsoft.EntityFrameworkCore;
@@ -59,6 +57,7 @@ public AuthenticationService(
5957
_configuration = configuration;
6058
_tokenService = tokenService;
6159
_userCreateService = userCreateService;
60+
6261

6362
var encryptKey = configuration.GetSection(AuthenticationConstants.EncryptKeyConfigKey).Value ?? "";
6463
var encryptIv = configuration.GetSection(AuthenticationConstants.EncryptIvConfigKey).Value ?? "";
@@ -90,26 +89,17 @@ public async Task SendResetPasswordEmail(SendResetPasswordEmailInput input)
9089
var logoIconUrl = $"{frontUrl}/icons/fin.png";
9190
var resetLink = $"{frontUrl}/authentication/reset-password?token={token}";
9291

93-
var subject = AuthenticationTemplates.ResetPasswordEmailSubject
94-
.Replace("{{appName}}", AppConstants.AppName);
95-
96-
var plainBody = AuthenticationTemplates.ResetPasswordEmailPlainTemplate
97-
.Replace("{{appName}}", AppConstants.AppName)
98-
.Replace("{{linkLifeTime}}", tokenLifeTimeInHours.ToString())
99-
.Replace("{{resetLink}}", resetLink);
92+
var parameters = new Dictionary<string, string>();
93+
parameters.Add("appName", AppConstants.AppName);
94+
parameters.Add("linkLifeTime", tokenLifeTimeInHours.ToString());
95+
parameters.Add("resetLink", resetLink);
96+
parameters.Add("logoIconUrl", logoIconUrl);
10097

101-
var htmlBody = AuthenticationTemplates.ResetPasswordEmailTemplate
102-
.Replace("{{appName}}", AppConstants.AppName)
103-
.Replace("{{logoIconUrl}}", logoIconUrl)
104-
.Replace("{{linkLifeTime}}", tokenLifeTimeInHours.ToString())
105-
.Replace("{{resetLink}}", resetLink);
106-
10798
await _emailSender.SendEmailAsync(new SendEmailDto
10899
{
109-
Subject = subject,
100+
BaseTemplatesName = "ResetPassword_",
101+
TemplateProperties = parameters,
110102
ToEmail = input.Email,
111-
PlainBody = plainBody,
112-
HtmlBody = htmlBody,
113103
ToName = credential.User.DisplayName
114104
});
115105
}

Fin.Application/Authentications/Utils/AuthenticationTemplates.cs

Lines changed: 0 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -109,160 +109,5 @@ public static class AuthenticationTemplates
109109
</script>
110110
</body>
111111
</html>
112-
";
113-
114-
public const string ResetPasswordEmailSubject = "{{appName}} - Reset Your Password";
115-
116-
public const string ResetPasswordEmailPlainTemplate = @"
117-
{{appName}} - Password Reset
118-
We received a request to reset your password.
119-
To create a new password, please copy and paste the link below into your browser:
120-
{{resetLink}}
121-
This link expires in {{linkLifeTime}} hours.
122-
If you didn't request this, please ignore this email.
123-
";
124-
125-
public const string ResetPasswordEmailTemplate = @"
126-
<!DOCTYPE html>
127-
<html lang='en'>
128-
<head>
129-
<meta charset='UTF-8'>
130-
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
131-
<title>Reset Your Password</title>
132-
<style>
133-
body {
134-
font-family: Arial, sans-serif;
135-
background-color: #f8f9fa;
136-
color: #212529;
137-
margin: 0;
138-
padding: 20px;
139-
}
140-
141-
.container {
142-
max-width: 600px;
143-
margin: 0 auto;
144-
background: white;
145-
border-radius: 8px;
146-
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
147-
}
148-
149-
.header {
150-
background: linear-gradient(135deg, #f87b07 0%, #fdc570 100%);
151-
padding: 30px;
152-
text-align: center;
153-
border-radius: 8px 8px 0 0;
154-
}
155-
156-
.app-icon {
157-
width: 60px;
158-
height: 60px;
159-
background: white;
160-
border-radius: 8px;
161-
display: inline-flex;
162-
align-items: center;
163-
justify-content: center;
164-
margin-bottom: 15px;
165-
}
166-
167-
.app-icon img {
168-
width: 40px;
169-
height: 40px;
170-
}
171-
172-
.app-name {
173-
color: white;
174-
font-size: 24px;
175-
font-weight: bold;
176-
margin: 0;
177-
}
178-
179-
.content {
180-
padding: 40px 30px;
181-
text-align: center;
182-
}
183-
184-
.title {
185-
font-size: 22px;
186-
color: rgb(46, 38, 26);
187-
margin-bottom: 20px;
188-
}
189-
190-
.message {
191-
color: #6c757d;
192-
margin-bottom: 30px;
193-
line-height: 1.5;
194-
}
195-
196-
.reset-button {
197-
display: inline-block;
198-
background: linear-gradient(135deg, #f87b07 0%, #fdc570 100%);
199-
color: white;
200-
padding: 15px 30px;
201-
text-decoration: none;
202-
border-radius: 6px;
203-
font-weight: bold;
204-
margin-bottom: 30px;
205-
}
206-
207-
.plain-link {
208-
background: #f8f9fa;
209-
padding: 15px;
210-
border-radius: 6px;
211-
margin: 20px 0;
212-
text-align: left;
213-
}
214-
215-
.plain-link p {
216-
margin: 0 0 10px 0;
217-
color: #6c757d;
218-
font-size: 14px;
219-
}
220-
221-
.plain-link a {
222-
color: #f87b07;
223-
word-break: break-all;
224-
font-size: 14px;
225-
}
226-
227-
.footer {
228-
background: rgb(46, 38, 26);
229-
color: #fdc570;
230-
padding: 20px 30px;
231-
text-align: center;
232-
border-radius: 0 0 8px 8px;
233-
font-size: 14px;
234-
}
235-
</style>
236-
</head>
237-
<body>
238-
<div class='container'>
239-
<div class='header'>
240-
<div class='app-icon'>
241-
<img src='{{logoIconUrl}}' alt='{{appName}} logo'>
242-
</div>
243-
<h1 class='app-name'>{{appName}}</h1>
244-
</div>
245-
246-
<div class='content'>
247-
<h2 class='title'>Reset Your Password</h2>
248-
249-
<p class='message'>
250-
We received a request to reset your password. Click the button below to create a new password.
251-
</p>
252-
253-
<a href='{{resetLink}}' class='reset-button'>Reset Password</a>
254-
255-
<div class='plain-link'>
256-
<p>Or copy and paste this link:</p>
257-
<a href='{{resetLink}}'>{{resetLink}}</a>
258-
</div>
259-
</div>
260-
261-
<div class='footer'>
262-
<p>This link expires in {{linkLifeTime}} hours. If you didn't request this, ignore this email.</p>
263-
</div>
264-
</div>
265-
</body>
266-
</html>
267112
";
268113
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using Fin.Infrastructure.AutoServices.Interfaces;
2+
using Fin.Infrastructure.EmailSenders.Constants;
3+
using Fin.Infrastructure.EmailSenders.Dto;
4+
using Fin.Infrastructure.EmailSenders.MailKit;
5+
using Fin.Infrastructure.EmailSenders.MailSender;
6+
using Microsoft.Extensions.Configuration;
7+
8+
namespace Fin.Application.Emails;
9+
10+
public interface IEmailSenderService
11+
{
12+
public Task<bool> SendEmailAsync(SendEmailDto dto, CancellationToken cancellationToken = default);
13+
}
14+
15+
public class EmailSenderService(
16+
IConfiguration configuration,
17+
IMailSenderClient mailSenderClient,
18+
IMailKitClient mailKitClient,
19+
IEmailTemplateService emailTemplateService
20+
) : IEmailSenderService, IAutoTransient
21+
{
22+
public async Task<bool> SendEmailAsync(SendEmailDto dto, CancellationToken cancellationToken = default)
23+
{
24+
PopulateWithTemplates(dto);
25+
26+
return GetMailService() switch
27+
{
28+
MailServicesConst.MailSender => await mailSenderClient.SendEmailAsync(dto, cancellationToken),
29+
_ => await mailKitClient.SendEmailAsync(dto, cancellationToken)
30+
};
31+
}
32+
33+
private string GetMailService()
34+
{
35+
var mailService = configuration.GetSection(MailServicesConst.MailServiceConfigurationKey).Value;
36+
return mailService ?? "";
37+
}
38+
39+
private void PopulateWithTemplates(SendEmailDto dto)
40+
{
41+
if (string.IsNullOrEmpty(dto.BaseTemplatesName)) return;
42+
43+
dto.TemplateProperties ??= new Dictionary<string, string>();
44+
45+
dto.HtmlBody ??= emailTemplateService.Get($"{dto.BaseTemplatesName}HTML", dto.TemplateProperties);
46+
dto.PlainBody ??= emailTemplateService.Get($"{dto.BaseTemplatesName}Plain", dto.TemplateProperties);
47+
dto.Subject ??= emailTemplateService.Get($"{dto.BaseTemplatesName}Subject", dto.TemplateProperties);
48+
}
49+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using Fin.Application.Resources.EmailTemplates;
2+
using Fin.Infrastructure.AutoServices.Interfaces;
3+
using Fin.Infrastructure.Constants;
4+
using Microsoft.Extensions.Configuration;
5+
6+
namespace Fin.Application.Emails;
7+
8+
public interface IEmailTemplateService
9+
{
10+
public string Get(string key);
11+
public string Get(string key, Dictionary<string, string> parameters);
12+
}
13+
14+
public class EmailTemplateService(IConfiguration configuration): IEmailTemplateService, IAutoTransient
15+
{
16+
public string Get(string key)
17+
{
18+
return EmailTemplates.ResourceManager.GetString(key);
19+
}
20+
21+
public string Get(string key, Dictionary<string, string> parameters)
22+
{
23+
var template = Get(key);
24+
25+
PopulateDefaultParameters(parameters);
26+
foreach (var parameter in parameters)
27+
{
28+
template = template.Replace("{{" + parameter.Key +"}}", parameter.Value);
29+
}
30+
return template;
31+
}
32+
33+
private void PopulateDefaultParameters(Dictionary<string, string> parameters)
34+
{
35+
var frontUrl = configuration.GetSection(AppConstants.FrontUrlConfigKey).Get<string>();
36+
var logoIconUrl = $"{frontUrl}/icons/fin.png";
37+
38+
parameters.TryAdd("appName", AppConstants.AppName);
39+
parameters.TryAdd("logoIconUrl", logoIconUrl);
40+
}
41+
}

0 commit comments

Comments
 (0)