Refactor identity generation logic (#108)

This commit is contained in:
Leendert de Borst
2024-08-05 17:24:51 +02:00
parent f43c3171b0
commit 73e432b2dc
19 changed files with 119 additions and 102 deletions

View File

@@ -28,14 +28,14 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Identity\Implementations\Lists\en\firstnames_female" />
<EmbeddedResource Include="Identity\Implementations\Lists\en\firstnames_male" />
<EmbeddedResource Include="Identity\Implementations\Lists\en\lastnames" />
<EmbeddedResource Include="Identity\Implementations\Lists\nl\firstnames_female" />
<EmbeddedResource Include="Identity\Implementations\Dictionaries\en\firstnames_female" />
<EmbeddedResource Include="Identity\Implementations\Dictionaries\en\firstnames_male" />
<EmbeddedResource Include="Identity\Implementations\Dictionaries\en\lastnames" />
<EmbeddedResource Include="Identity\Implementations\Dictionaries\nl\firstnames_female" />
<None Remove="Identity\Implementations\Lists\nl\firstnames" />
<EmbeddedResource Include="Identity\Implementations\Lists\nl\firstnames_male" />
<EmbeddedResource Include="Identity\Implementations\Dictionaries\nl\firstnames_male" />
<None Remove="Identity\Implementations\Lists\nl\lastnames" />
<EmbeddedResource Include="Identity\Implementations\Lists\nl\lastnames" />
<EmbeddedResource Include="Identity\Implementations\Dictionaries\nl\lastnames" />
</ItemGroup>
</Project>

View File

@@ -50,17 +50,17 @@ public abstract class IdentityGenerator : IIdentityGenerator
/// <summary>
/// Gets namespace path to the male first names list for the correct language.
/// </summary>
protected virtual string FirstNamesListMale => "AliasGenerators.Identity.Implementations.Lists.nl.firstnames_male";
protected virtual string FirstNamesListMale => "AliasGenerators.Identity.Implementations.Dictionaries.nl.firstnames_male";
/// <summary>
/// Gets namespace path to the female first names list for the correct language.
/// </summary>
protected virtual string FirstNamesListFemale => "AliasGenerators.Identity.Implementations.Lists.nl.firstnames_female";
protected virtual string FirstNamesListFemale => "AliasGenerators.Identity.Implementations.Dictionaries.nl.firstnames_female";
/// <summary>
/// Gets namespace path to the last names list for the correct language.
/// </summary>
protected virtual string LastNamesList => "AliasGenerators.Identity.Implementations.Lists.nl.lastnames";
protected virtual string LastNamesList => "AliasGenerators.Identity.Implementations.Dictionaries.nl.lastnames";
/// <inheritdoc/>
public async Task<Identity> GenerateRandomIdentityAsync()

View File

@@ -0,0 +1,32 @@
//-----------------------------------------------------------------------
// <copyright file="IdentityGeneratorFactory.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasGenerators.Identity.Implementations;
using AliasGenerators.Identity.Implementations.Base;
/// <summary>
/// Identity generator factory which creates an identity generator based on the language code.
/// </summary>
public static class IdentityGeneratorFactory
{
/// <summary>
/// Creates an identity generator based on the language code.
/// </summary>
/// <param name="languageCode">Two letter language code.</param>
/// <returns>The IdentityGenerator for the requested language.</returns>
/// <exception cref="ArgumentException">Thrown if no identity generator is found for the requested language.</exception>
public static IdentityGenerator CreateIdentityGenerator(string languageCode)
{
return languageCode.ToLower() switch
{
"nl" => new IdentityGeneratorNl(),
"en" => new IdentityGeneratorEn(),
_ => throw new ArgumentException($"Unsupported language code: {languageCode}", nameof(languageCode)),
};
}
}

View File

@@ -1,40 +0,0 @@
//-----------------------------------------------------------------------
// <copyright file="FigIdentityGenerator.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasGenerators.Identity.Implementations;
using System.Text.Json;
/// <summary>
/// Identity generator which generates random identities using the identiteitgenerator.nl semi-public API.
/// </summary>
public class FigIdentityGenerator : IIdentityGenerator
{
private static readonly HttpClient HttpClient = new();
private static readonly string Url = "https://api.identiteitgenerator.nl/generate/identity";
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
};
/// <inheritdoc/>
public async Task<Identity.Models.Identity> GenerateRandomIdentityAsync()
{
var response = await HttpClient.GetAsync(Url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var identity = JsonSerializer.Deserialize<Identity.Models.Identity>(json, JsonSerializerOptions);
if (identity is null)
{
throw new InvalidOperationException("Failed to deserialize the identity from FIG WebApi.");
}
return identity;
}
}

View File

@@ -16,11 +16,11 @@ using AliasGenerators.Identity.Implementations.Base;
public class IdentityGeneratorEn : IdentityGenerator
{
/// <inheritdoc cref="IdentityGenerator.FirstNamesListMale" />
protected override string FirstNamesListMale => "AliasGenerators.Identity.Implementations.Lists.en.firstnames_male";
protected override string FirstNamesListMale => "AliasGenerators.Identity.Implementations.Dictionaries.en.firstnames_male";
/// <inheritdoc cref="IdentityGenerator.FirstNamesListFemale" />
protected override string FirstNamesListFemale => "AliasGenerators.Identity.Implementations.Lists.en.firstnames_female";
protected override string FirstNamesListFemale => "AliasGenerators.Identity.Implementations.Dictionaries.en.firstnames_female";
/// <inheritdoc cref="IdentityGenerator.LastNamesList" />
protected override string LastNamesList => "AliasGenerators.Identity.Implementations.Lists.en.lastnames";
protected override string LastNamesList => "AliasGenerators.Identity.Implementations.Dictionaries.en.lastnames";
}

View File

@@ -16,11 +16,11 @@ using AliasGenerators.Identity.Implementations.Base;
public class IdentityGeneratorNl : IdentityGenerator
{
/// <inheritdoc cref="IdentityGenerator.FirstNamesListMale" />
protected override string FirstNamesListMale => "AliasGenerators.Identity.Implementations.Lists.nl.firstnames_male";
protected override string FirstNamesListMale => "AliasGenerators.Identity.Implementations.Dictionaries.nl.firstnames_male";
/// <inheritdoc cref="IdentityGenerator.FirstNamesListFemale" />
protected override string FirstNamesListFemale => "AliasGenerators.Identity.Implementations.Lists.nl.firstnames_female";
protected override string FirstNamesListFemale => "AliasGenerators.Identity.Implementations.Dictionaries.nl.firstnames_female";
/// <inheritdoc cref="IdentityGenerator.LastNamesList" />
protected override string LastNamesList => "AliasGenerators.Identity.Implementations.Lists.nl.lastnames";
protected override string LastNamesList => "AliasGenerators.Identity.Implementations.Dictionaries.nl.lastnames";
}

View File

@@ -1,27 +0,0 @@
//-----------------------------------------------------------------------
// <copyright file="StaticIdentityGenerator.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasGenerators.Identity.Implementations;
using AliasGenerators.Identity;
/// <summary>
/// Static identity generator which implements IIdentityGenerator but always returns
/// the same static identity for testing purposes.
/// </summary>
public class StaticIdentityGenerator : IIdentityGenerator
{
/// <inheritdoc/>
public async Task<Identity.Models.Identity> GenerateRandomIdentityAsync()
{
await Task.Yield(); // Add an await statement to make the method truly asynchronous.
return new Identity.Models.Identity
{
FirstName = "John",
LastName = "Doe",
};
}
}

View File

@@ -22,6 +22,9 @@ public class UsernameEmailGenerator
/// </summary>
private const int MaxLength = 20;
/// <summary>
/// Create a new random instance for generating random values.
/// </summary>
private readonly Random _random = new();
/// <summary>
@@ -60,7 +63,7 @@ public class UsernameEmailGenerator
/// <returns>Valid email prefix as string.</returns>
public string GenerateEmailPrefix(Models.Identity identity)
{
List<string> parts = new List<string>();
var parts = new List<string>();
// Use first initial + last name
if (_random.Next(2) == 0)
@@ -86,7 +89,7 @@ public class UsernameEmailGenerator
}
// Join parts and sanitize
string emailPrefix = string.Join(GetRandomSymbol(), parts);
var emailPrefix = string.Join(GetRandomSymbol(), parts);
emailPrefix = SanitizeEmailPrefix(emailPrefix);
// Adjust length
@@ -103,15 +106,11 @@ public class UsernameEmailGenerator
}
/// <summary>
/// Get a random symbol from the list of symbols.
/// Sanitize the email prefix by removing invalid characters and ensuring it's a valid email prefix.
/// </summary>
/// <returns>Random symbol.</returns>
private string GetRandomSymbol()
{
return _random.Next(3) == 0 ? _symbols[_random.Next(_symbols.Count)] : string.Empty;
}
private string SanitizeEmailPrefix(string input)
/// <param name="input">The input string to sanitize.</param>
/// <returns>The sanitized string.</returns>
private static string SanitizeEmailPrefix(string input)
{
// Remove any character that's not a letter, number, dot, underscore, or hyphen including special characters
string sanitized = System.Text.RegularExpressions.Regex.Replace(input, @"[^a-zA-Z0-9._-]", string.Empty);
@@ -125,6 +124,20 @@ public class UsernameEmailGenerator
return sanitized;
}
/// <summary>
/// Get a random symbol from the list of symbols.
/// </summary>
/// <returns>Random symbol.</returns>
private string GetRandomSymbol()
{
return _random.Next(3) == 0 ? _symbols[_random.Next(_symbols.Count)] : string.Empty;
}
/// <summary>
/// Generate a random string of a given length.
/// </summary>
/// <param name="length">Length of string to generate.</param>
/// <returns>String with random characters.</returns>
private string GenerateRandomString(int length)
{
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";

View File

@@ -21,7 +21,7 @@ using Microsoft.AspNetCore.Mvc;
public class IdentityController(UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
{
/// <summary>
/// Proxies the request to the identity generator to generate a random identity.
/// Proxies the request to the english identity generator to generate a random identity.
/// </summary>
/// <returns>Identity model.</returns>
[HttpGet("Generate")]
@@ -33,7 +33,7 @@ public class IdentityController(UserManager<AliasVaultUser> userManager) : Authe
return Unauthorized();
}
var identityGenerator = new FigIdentityGenerator();
var identityGenerator = new IdentityGeneratorEn();
return Ok(await identityGenerator.GenerateRandomIdentityAsync());
}
}

View File

@@ -244,7 +244,7 @@ else
StateHasChanged();
// Generate a random identity using the IIdentityGenerator implementation.
var identity = await new IdentityGeneratorNl().GenerateRandomIdentityAsync();
var identity = await IdentityGeneratorFactory.CreateIdentityGenerator(DbService.Settings.DefaultIdentityLanguage).GenerateRandomIdentityAsync();
// Generate random values for the Identity properties
Obj.Username = identity.NickName;

View File

@@ -44,6 +44,21 @@
</div>
</div>
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Identity Settings</h3>
<div class="mb-4">
<label for="defaultEmailDomain" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Identity generation language</label>
<select @bind="DefaultIdentityLanguage" @bind:after="UpdateDefaultIdentityLanguage" id="defaultIdentityLanguage" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option value="en">English</option>
<option value="nl">Dutch</option>
</select>
<span class="block text-sm font-normal text-gray-500 truncate dark:text-gray-400">
Set the default language that will be used when generating new identities.
</span>
</div>
</div>
@code {
private List<string> PrivateDomains => Config.PrivateEmailDomains;
private List<string> PublicDomains => Config.PublicEmailDomains;
@@ -51,6 +66,7 @@
private string DefaultEmailDomain { get; set; } = string.Empty;
private bool AutoEmailRefresh { get; set; }
private string DefaultIdentityLanguage { get; set; } = string.Empty;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
@@ -60,6 +76,7 @@
DefaultEmailDomain = DbService.Settings.DefaultEmailDomain;
AutoEmailRefresh = DbService.Settings.AutoEmailRefresh;
DefaultIdentityLanguage = DbService.Settings.DefaultIdentityLanguage;
}
/// <summary>
@@ -79,4 +96,13 @@
await DbService.Settings.SetAutoEmailRefresh(AutoEmailRefresh);
StateHasChanged();
}
/// <summary>
/// Updates the auto email refresh setting.
/// </summary>
private async Task UpdateDefaultIdentityLanguage()
{
await DbService.Settings.SetDefaultIdentityLanguage(DefaultIdentityLanguage);
StateHasChanged();
}
}

View File

@@ -27,7 +27,7 @@ public class SettingsService
private bool _initialized;
/// <summary>
/// Gets the DefaultEmailDomain setting asynchronously.
/// Gets the DefaultEmailDomain setting.
/// </summary>
/// <returns>Default email domain as string.</returns>
public string DefaultEmailDomain => GetSetting("DefaultEmailDomain");
@@ -39,19 +39,32 @@ public class SettingsService
public bool AutoEmailRefresh => GetSetting<bool>("AutoEmailRefresh", true);
/// <summary>
/// Sets the DefaultEmailDomain setting asynchronously.
/// Gets the DefaultIdentityLanguage setting.
/// </summary>
/// <param name="value">The new DeafultEmailDomain setting.</param>
/// <returns>Default identity language as two-letter code.</returns>
public string DefaultIdentityLanguage => GetSetting<string>("DefaultIdentityLanguage", "en")!;
/// <summary>
/// Sets the DefaultEmailDomain setting.
/// </summary>
/// <param name="value">The new DefaultEmailDomain setting.</param>
/// <returns>Task.</returns>
public Task SetDefaultEmailDomain(string value) => SetSettingAsync("DefaultEmailDomain", value);
/// <summary>
/// Sets the AutoEmailRefresh setting asynchronously as a string.
/// Sets the AutoEmailRefresh setting as a string.
/// </summary>
/// <param name="value">The new value.</param>
/// <returns>Task.</returns>
public Task SetAutoEmailRefresh(bool value) => SetSettingAsync<bool>("AutoEmailRefresh", value);
/// <summary>
/// Sets the DefaultIdentityLanguage setting.
/// </summary>
/// <param name="value">The new value.</param>
/// <returns>Task.</returns>
public Task SetDefaultIdentityLanguage(string value) => SetSettingAsync("DefaultIdentityLanguage", value);
/// <summary>
/// Initializes the settings service asynchronously.
/// </summary>

View File

@@ -24,7 +24,7 @@ public class IdentityGeneratorTest
public async Task TestNlGenerator()
{
// Generate random NL identity.
var generator = new IdentityGeneratorNl();
var generator = IdentityGeneratorFactory.CreateIdentityGenerator("nl");
// Generate 10 identities, check if they are all unique.
var identities = new List<Identity>();
@@ -62,8 +62,8 @@ public class IdentityGeneratorTest
[Test]
public async Task TestEnGenerator()
{
// Generate random NL identity.
var generator = new IdentityGeneratorEn();
// Generate random EN identity.
var generator = IdentityGeneratorFactory.CreateIdentityGenerator("en");
// Generate 10 identities, check if they are all unique.
var identities = new List<Identity>();