Skip to content
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
11 changes: 9 additions & 2 deletions Fin-Backend.sln.DotSettings.user
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,22 @@
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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;
<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;
&lt;Solution /&gt;
&lt;/SessionState&gt;</s:String>
<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;
<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;
&lt;Project Location="/home/rafaelchicovis/git/fin-backend/Fin.Test" Presentation="&amp;lt;Fin.Test&amp;gt;" /&gt;
&lt;/SessionState&gt;</s:String>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=Fin_002EApplication_002FResources_002FEmailTemplates_002FEmailTemplates/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=Fin_002EApplication_002FResources_002FEmailTemplates_002FResources_002Ees_002DEN/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=Fin_002EApplication_002FResources_002FEmailTemplates_002FResources_002Ees_002DEN/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=Fin_002EApplication_002FResources_002FEmailTemplates_002FResources/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=Fin_002EApplication_002FResources_002FEmailTemplates_002FResources/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/Initialized/@EntryValue">True</s:Boolean>



Expand Down
5 changes: 3 additions & 2 deletions Fin.Api/Fin.Api.csproj
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
Expand Down
18 changes: 18 additions & 0 deletions Fin.Api/Tenants/TenantController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Fin.Application.Tenants;
using Fin.Application.Tenants.Dtos;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Fin.Api.Tenants;

[Route("tenants")]
[Authorize]
public class TenantController(ITenantService service): ControllerBase
{
[HttpGet("{id:guid}")]
public async Task<ActionResult<TenantOutput>> Get([FromRoute] Guid id)
{
var menu = await service.Get(id);
return menu != null ? Ok(menu) : NotFound();
}
}
28 changes: 9 additions & 19 deletions Fin.Application/Authentications/Services/AuthenticationService.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
using Fin.Application.Authentications.Dtos;
using Fin.Application.Authentications.Enums;
using Fin.Application.Authentications.Utils;
using Fin.Application.Emails;
using Fin.Application.Globals.Dtos;
using Fin.Application.Users.Services;
using Fin.Domain.Global;
using Fin.Domain.Tenants.Entities;
using Fin.Domain.Users.Dtos;
using Fin.Domain.Users.Entities;
using Fin.Infrastructure.Authentications;
Expand All @@ -14,7 +13,6 @@
using Fin.Infrastructure.AutoServices.Interfaces;
using Fin.Infrastructure.Constants;
using Fin.Infrastructure.Database.Repositories;
using Fin.Infrastructure.EmailSenders;
using Fin.Infrastructure.EmailSenders.Dto;
using Fin.Infrastructure.Redis;
using Microsoft.EntityFrameworkCore;
Expand Down Expand Up @@ -59,6 +57,7 @@ public AuthenticationService(
_configuration = configuration;
_tokenService = tokenService;
_userCreateService = userCreateService;


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

var subject = AuthenticationTemplates.ResetPasswordEmailSubject
.Replace("{{appName}}", AppConstants.AppName);

var plainBody = AuthenticationTemplates.ResetPasswordEmailPlainTemplate
.Replace("{{appName}}", AppConstants.AppName)
.Replace("{{linkLifeTime}}", tokenLifeTimeInHours.ToString())
.Replace("{{resetLink}}", resetLink);
var parameters = new Dictionary<string, string>();
parameters.Add("appName", AppConstants.AppName);
parameters.Add("linkLifeTime", tokenLifeTimeInHours.ToString());
parameters.Add("resetLink", resetLink);
parameters.Add("logoIconUrl", logoIconUrl);

var htmlBody = AuthenticationTemplates.ResetPasswordEmailTemplate
.Replace("{{appName}}", AppConstants.AppName)
.Replace("{{logoIconUrl}}", logoIconUrl)
.Replace("{{linkLifeTime}}", tokenLifeTimeInHours.ToString())
.Replace("{{resetLink}}", resetLink);

await _emailSender.SendEmailAsync(new SendEmailDto
{
Subject = subject,
BaseTemplatesName = "ResetPassword_",
TemplateProperties = parameters,
ToEmail = input.Email,
PlainBody = plainBody,
HtmlBody = htmlBody,
ToName = credential.User.DisplayName
});
}
Expand Down
155 changes: 0 additions & 155 deletions Fin.Application/Authentications/Utils/AuthenticationTemplates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,160 +109,5 @@ public static class AuthenticationTemplates
</script>
</body>
</html>
";

public const string ResetPasswordEmailSubject = "{{appName}} - Reset Your Password";

public const string ResetPasswordEmailPlainTemplate = @"
{{appName}} - Password Reset
We received a request to reset your password.
To create a new password, please copy and paste the link below into your browser:
{{resetLink}}
This link expires in {{linkLifeTime}} hours.
If you didn't request this, please ignore this email.
";

public const string ResetPasswordEmailTemplate = @"
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Reset Your Password</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f8f9fa;
color: #212529;
margin: 0;
padding: 20px;
}

.container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.header {
background: linear-gradient(135deg, #f87b07 0%, #fdc570 100%);
padding: 30px;
text-align: center;
border-radius: 8px 8px 0 0;
}

.app-icon {
width: 60px;
height: 60px;
background: white;
border-radius: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
}

.app-icon img {
width: 40px;
height: 40px;
}

.app-name {
color: white;
font-size: 24px;
font-weight: bold;
margin: 0;
}

.content {
padding: 40px 30px;
text-align: center;
}

.title {
font-size: 22px;
color: rgb(46, 38, 26);
margin-bottom: 20px;
}

.message {
color: #6c757d;
margin-bottom: 30px;
line-height: 1.5;
}

.reset-button {
display: inline-block;
background: linear-gradient(135deg, #f87b07 0%, #fdc570 100%);
color: white;
padding: 15px 30px;
text-decoration: none;
border-radius: 6px;
font-weight: bold;
margin-bottom: 30px;
}

.plain-link {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
margin: 20px 0;
text-align: left;
}

.plain-link p {
margin: 0 0 10px 0;
color: #6c757d;
font-size: 14px;
}

.plain-link a {
color: #f87b07;
word-break: break-all;
font-size: 14px;
}

.footer {
background: rgb(46, 38, 26);
color: #fdc570;
padding: 20px 30px;
text-align: center;
border-radius: 0 0 8px 8px;
font-size: 14px;
}
</style>
</head>
<body>
<div class='container'>
<div class='header'>
<div class='app-icon'>
<img src='{{logoIconUrl}}' alt='{{appName}} logo'>
</div>
<h1 class='app-name'>{{appName}}</h1>
</div>

<div class='content'>
<h2 class='title'>Reset Your Password</h2>

<p class='message'>
We received a request to reset your password. Click the button below to create a new password.
</p>

<a href='{{resetLink}}' class='reset-button'>Reset Password</a>

<div class='plain-link'>
<p>Or copy and paste this link:</p>
<a href='{{resetLink}}'>{{resetLink}}</a>
</div>
</div>

<div class='footer'>
<p>This link expires in {{linkLifeTime}} hours. If you didn't request this, ignore this email.</p>
</div>
</div>
</body>
</html>
";
}
49 changes: 49 additions & 0 deletions Fin.Application/Emails/EmailSenderService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Fin.Infrastructure.AutoServices.Interfaces;
using Fin.Infrastructure.EmailSenders.Constants;
using Fin.Infrastructure.EmailSenders.Dto;
using Fin.Infrastructure.EmailSenders.MailKit;
using Fin.Infrastructure.EmailSenders.MailSender;
using Microsoft.Extensions.Configuration;

namespace Fin.Application.Emails;

public interface IEmailSenderService
{
public Task<bool> SendEmailAsync(SendEmailDto dto, CancellationToken cancellationToken = default);
}

public class EmailSenderService(
IConfiguration configuration,
IMailSenderClient mailSenderClient,
IMailKitClient mailKitClient,
IEmailTemplateService emailTemplateService
) : IEmailSenderService, IAutoTransient
{
public async Task<bool> SendEmailAsync(SendEmailDto dto, CancellationToken cancellationToken = default)
{
PopulateWithTemplates(dto);

return GetMailService() switch
{
MailServicesConst.MailSender => await mailSenderClient.SendEmailAsync(dto, cancellationToken),
_ => await mailKitClient.SendEmailAsync(dto, cancellationToken)
};
}

private string GetMailService()
{
var mailService = configuration.GetSection(MailServicesConst.MailServiceConfigurationKey).Value;
return mailService ?? "";
}

private void PopulateWithTemplates(SendEmailDto dto)
{
if (string.IsNullOrEmpty(dto.BaseTemplatesName)) return;

dto.TemplateProperties ??= new Dictionary<string, string>();

dto.HtmlBody ??= emailTemplateService.Get($"{dto.BaseTemplatesName}HTML", dto.TemplateProperties);
dto.PlainBody ??= emailTemplateService.Get($"{dto.BaseTemplatesName}Plain", dto.TemplateProperties);
dto.Subject ??= emailTemplateService.Get($"{dto.BaseTemplatesName}Subject", dto.TemplateProperties);
}
}
41 changes: 41 additions & 0 deletions Fin.Application/Emails/EmailTemplateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Fin.Application.Resources.EmailTemplates;
using Fin.Infrastructure.AutoServices.Interfaces;
using Fin.Infrastructure.Constants;
using Microsoft.Extensions.Configuration;

namespace Fin.Application.Emails;

public interface IEmailTemplateService
{
public string Get(string key);
public string Get(string key, Dictionary<string, string> parameters);
}

public class EmailTemplateService(IConfiguration configuration): IEmailTemplateService, IAutoTransient
{
public string Get(string key)
{
return EmailTemplates.ResourceManager.GetString(key);
}

public string Get(string key, Dictionary<string, string> parameters)
{
var template = Get(key);

PopulateDefaultParameters(parameters);
foreach (var parameter in parameters)
{
template = template.Replace("{{" + parameter.Key +"}}", parameter.Value);
}
return template;
}

private void PopulateDefaultParameters(Dictionary<string, string> parameters)
{
var frontUrl = configuration.GetSection(AppConstants.FrontUrlConfigKey).Get<string>();
var logoIconUrl = $"{frontUrl}/icons/fin.png";

parameters.TryAdd("appName", AppConstants.AppName);
parameters.TryAdd("logoIconUrl", logoIconUrl);
}
}
Loading
Loading