Skip to content

Commit

Permalink
Merge pull request #44 from galliumplus/feature/users
Browse files Browse the repository at this point in the history
Changement de mot de passe et envoi de mail
  • Loading branch information
louisdevie authored Oct 29, 2023
2 parents a9092d6 + 5ab4ca9 commit 7f30774
Show file tree
Hide file tree
Showing 23 changed files with 741 additions and 89 deletions.
4 changes: 3 additions & 1 deletion Core/Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Humanizer.Core.fr" Version="2.14.1" />
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.0" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="13.3.0" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="13.4.0" />
<PackageReference Include="Stubble.Core" Version="1.10.8" />
</ItemGroup>

</Project>
98 changes: 98 additions & 0 deletions Core/Email/CachedLocalEmailTemplateLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using Stubble.Core.Interfaces;
using System.Collections.Concurrent;
using System.Text;

namespace GalliumPlus.WebApi.Core.Email
{
/// <summary>
/// Un chargeur de modèles qui va récupère les données depuis le système de
/// fichiers local et les mets en cache. Les modèles sont identifiés par leur
/// nom de fichier.
/// </summary>
public class CachedLocalEmailTemplateLoader : IEmailTemplateLoader, IStubbleLoader
{
private string baseDirectory;
private Dictionary<string, string> cache;

/// <summary>
/// Crée un nouveau chargeur local.
/// </summary>
/// <param name="baseDirectory">Le chemin absolu du dossier contenant les modèles.</param>
public CachedLocalEmailTemplateLoader(string baseDirectory)
{
this.baseDirectory = baseDirectory;
this.cache = new();
}

public IStubbleLoader Clone()
{
var loader = new CachedLocalEmailTemplateLoader(this.baseDirectory)
{
cache = new(this.cache)
};
return loader;
}

public string Load(string name)
{
string? template;
bool cached;

lock (this.cache)
{
cached = this.cache.TryGetValue(name, out template);
}

if (!cached)
{
try
{
using (StreamReader f = new(Path.Join(this.baseDirectory, name)))
{
template = f.ReadToEnd();
}
lock (this.cache)
{
cache.TryAdd(name, template);
}
}
catch (FileNotFoundException err)
{
Console.WriteLine(err.Message);
template = null;
}
}

return template!;
}

public async ValueTask<string> LoadAsync(string name)
{
string? template;

if (!cache.TryGetValue(name, out template))
{
try
{
using (StreamReader f = new(Path.Join(this.baseDirectory, name), Encoding.UTF8))
{
template = await f.ReadToEndAsync();
}
cache.Add(name, template);
}
catch (FileNotFoundException err)
{
Console.WriteLine(err.Message);
template = null;
}
}

return template!;
}

public EmailTemplate GetTemplate(string identifier)
{
return new EmailTemplate(identifier, this);
}
}
}
51 changes: 51 additions & 0 deletions Core/Email/EmailTemplate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Stubble.Core;
using Stubble.Core.Builders;
using Stubble.Core.Interfaces;

namespace GalliumPlus.WebApi.Core.Email
{
/// <summary>
/// Un modèle de mail.
/// </summary>
public class EmailTemplate
{
private string name;
private StubbleVisitorRenderer renderer;

/// <summary>
/// Crée un nouveau modèle de mail.
/// </summary>
/// <param name="name">Le nom du modèle.</param>
/// <param name="partialTemplateLoader">(optionnel) Le chargeur de modèles partiels.</param>
public EmailTemplate(string name, IStubbleLoader? partialTemplateLoader)
{
this.name = name;

this.renderer = new StubbleBuilder()
.Configure(settings =>
{
if (partialTemplateLoader is not null)
{
settings.AddToTemplateLoader(partialTemplateLoader);
}
})
.Build();
}

/// <summary>
/// Imprime un corps de mail à partir des données fournies.
/// </summary>
/// <param name="view">Les données à utiliser pour remplir le modèle.</param>
/// <returns>Le corps du mail.</returns>
public string Render(object view)
{
return this.renderer.Render(this.name, view);
}

/// <inheritdoc cref="Render(object)"/>
public ValueTask<string> RenderAsync(object view)
{
return this.renderer.RenderAsync(this.name, view);
}
}
}
19 changes: 19 additions & 0 deletions Core/Email/IEmailSender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace GalliumPlus.WebApi.Core.Email
{
/// <summary>
/// Un système d'envoi de mail.
/// </summary>
public interface IEmailSender
{
/// <summary>
/// Envoie un mail.
/// </summary>
/// <param name="recipient">L'adresse mail du destinataire.</param>
/// <param name="subject">Le sujet du mail.</param>
/// <param name="content">Le contenu du mail (typiquement le résultat de <see cref="EmailTemplate.Render"/>).</param>
void Send(string recipient, string subject, string content);

/// <inheritdoc cref="Send"/>
Task SendAsync(string recipient, string subject, string content, CancellationToken ct = default);
}
}
14 changes: 14 additions & 0 deletions Core/Email/IEmailTemplateLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace GalliumPlus.WebApi.Core.Email
{
/// <summary>
/// Un chargeur de modèles. Son rôle est de préparer des <see cref="EmailTemplate"/>.
/// </summary>
public interface IEmailTemplateLoader
{
/// <summary>
/// Charge un modèle de mail.
/// </summary>
/// <param name="identifier">Une chaîne de caractère permettant d'identifier le modèle.</param>
EmailTemplate GetTemplate(string identifier);
}
}
40 changes: 40 additions & 0 deletions Core/Email/TemplateViews/InitOrResetPassword.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Humanizer;
using System.Globalization;

namespace GalliumPlus.WebApi.Core.Email.TemplateViews
{
/// <summary>
/// Le contenu dynamique d'un mail d'initialisation de compte ou de réinitialisation de mot de passe.
/// <br/>
/// (Ces mails sont envoyé à l'utilisateur lorsqu'un nouveau compte est créé ou qu'il a oublié son mot de passe)
/// </summary>
public class InitOrResetPassword
{
/// <summary>
/// Le lien permettant d'activer le compte/changer le mot de passe.
/// </summary>
public string Link { get; private init; }

/// <summary>
/// L'heure d'expiration du lien (<see cref="Link"/>).
/// </summary>
public DateTime Expiration { get; private init; }

/// <summary>
/// Une version humaine de l'heure d'expiration du lien (par ex. <em>« dans 2 heures »</em>).
/// </summary>
public string HumanExpiration => this.Expiration.Humanize(utcDate: true, culture: new CultureInfo("fr-FR"));

/// <summary>
/// Rassemble les données pour un nouveau mail de (ré)initialisation.
/// </summary>
/// <param name="link">L'URL complète de la page de saisie de mot de passe.</param>
/// <param name="expiration">L'heure d'expiration du lien, en temps UTC.</param>
public InitOrResetPassword(string link, DateTime expiration)
{
this.Link = link;
if (expiration.Kind != DateTimeKind.Utc) throw new ArgumentException("The link expiration must be expressed in UTC.");
this.Expiration = expiration;
}
}
}
42 changes: 42 additions & 0 deletions Core/Users/PasswordResetToken.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using GalliumPlus.WebApi.Core.Applications;
using GalliumPlus.WebApi.Core.Exceptions;
using GalliumPlus.WebApi.Core.Random;

namespace GalliumPlus.WebApi.Core.Users
Expand Down Expand Up @@ -100,5 +101,46 @@ public bool MatchesSecret(string secret)
{
return this.secret.Match(secret);
}

/// <summary>
/// Concatène un jeton et un secret ensemble.
/// </summary>
/// <param name="token">Un jeton de réinitialisation.</param>
/// <param name="secret">Un code secret de réinitialisation.</param>
/// <returns>Une chaîne de caractères contenant le jeton et le code secret, utilisable dans un URL.</returns>
public static string Pack(string token, string secret)
{
return String.Join(':', token, secret);
}

/// <summary>
/// Génère le code secret et le concatène au jeton.
/// </summary>
/// <returns>
/// Une chaîne de caractères contenant le jeton et le code secret généré,
/// utilisable dans un URL. Une fois cette valeur oubliée, elle ne peut
/// plus être récupérée.
/// </returns>
public string GenerateSecretAndPack()
{
string secret = this.GenerateSecret();
return Pack(this.token, secret);
}

/// <summary>
/// Sépare un token et un code secret empaquetés.
/// </summary>
/// <param name="packedPrt">Les données empaquetées.</param>
/// <returns>Le jeton et le code secret, respectivement.</returns>
/// <exception cref="InvalidItemException"/>
public static (string, string) Unpack(string packedPrt)
{
string[] parts = packedPrt.Split(':');
if (parts.Length != 2)
{
throw new InvalidItemException("Jeton de réinitialisation invalide.");
}
return (parts[0], parts[1]);
}
}
}
8 changes: 8 additions & 0 deletions Gallium+ API.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MariaDb", "MariaDb\MariaDb.csproj", "{0091163D-F909-442C-9397-859D8E069322}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailKitClient", "MailKitClient\MailKitClient.csproj", "{28D2F1D5-BC2B-4C00-992F-3C4B0AD3D9DF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -56,6 +58,12 @@ Global
{0091163D-F909-442C-9397-859D8E069322}.Release|Any CPU.Build.0 = Release|Any CPU
{0091163D-F909-442C-9397-859D8E069322}.Test|Any CPU.ActiveCfg = Debug|Any CPU
{0091163D-F909-442C-9397-859D8E069322}.Test|Any CPU.Build.0 = Debug|Any CPU
{28D2F1D5-BC2B-4C00-992F-3C4B0AD3D9DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{28D2F1D5-BC2B-4C00-992F-3C4B0AD3D9DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28D2F1D5-BC2B-4C00-992F-3C4B0AD3D9DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28D2F1D5-BC2B-4C00-992F-3C4B0AD3D9DF}.Release|Any CPU.Build.0 = Release|Any CPU
{28D2F1D5-BC2B-4C00-992F-3C4B0AD3D9DF}.Test|Any CPU.ActiveCfg = Debug|Any CPU
{28D2F1D5-BC2B-4C00-992F-3C4B0AD3D9DF}.Test|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Loading

0 comments on commit 7f30774

Please sign in to comment.