From ed80ad24c1633efd15d686c31964e9e5dd6c687a Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sun, 12 Jan 2025 13:02:22 +0000 Subject: [PATCH 1/3] Add more names to identity generator dictionary to prevent collisions (#525) --- .../Dictionaries/en/firstnames_female | 106 +++++++++++++++++ .../Dictionaries/en/firstnames_male | 105 +++++++++++++++++ .../Implementations/Dictionaries/en/lastnames | 104 ++++++++++++++++ .../Dictionaries/nl/firstnames_female | 107 +++++++++++++++++ .../Dictionaries/nl/firstnames_male | 111 ++++++++++++++++++ .../Implementations/Dictionaries/nl/lastnames | 101 ++++++++++++++++ .../UsernameEmailGenerator.cs | 54 +++++++-- 7 files changed, 676 insertions(+), 12 deletions(-) diff --git a/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/en/firstnames_female b/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/en/firstnames_female index dfc32a1e2..4f10e1992 100644 --- a/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/en/firstnames_female +++ b/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/en/firstnames_female @@ -151,3 +151,109 @@ Juliana Charlie Lucia Stella +Adriana +Beatrice +Bianca +Calliope +Carmen +Celeste +Dakota +Diana +Esther +Florence +Francesca +Georgia +Harlow +Haven +Holly +Hope +India +Indie +Iris +Juniper +Kaia +Keira +Lara +Laura +Laurel +Luna +Magnolia +Maeve +Marina +Marlowe +Nina +Noelle +Octavia +Olive +Ophelia +Phoenix +Poppy +Primrose +Ramona +River +Rosalie +Rosemary +Sage +Salem +Selena +Sienna +Summer +Sylvie +Thea +Tessa +Wren +Winter +Willa +Ada +Aspen +Blair +Brynn +Cassidy +Cecilia +Daisy +Dawn +Daphne +Ember +Fiona +Flora +Freya +Gemma +Giselle +Harmony +Heidi +Imogen +Indie +Jessie +June +Kaia +Lena +Lola +Mabel +Maisie +Margot +Matilda +Mira +Morgan +Nell +Nadia +Odette +Opal +Pearl +Phoebe +Raven +Reese +Robin +Rowan +Ruth +Sabrina +Sasha +Sierra +Skye +Sloane +Talia +Thora +Vera +Willa +Winnie +Yara +Zara diff --git a/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/en/firstnames_male b/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/en/firstnames_male index ceddb7bd2..69cec3610 100644 --- a/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/en/firstnames_male +++ b/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/en/firstnames_male @@ -140,3 +140,108 @@ Levi Alan Jorge Carson +Felix +Oliver +Theodore +Harrison +Maxwell +Sebastian +Xavier +Dominick +Lincoln +Elliott +Walter +Simon +Dean +Hugo +Malcolm +Leon +Oscar +Calvin +Raymond +Edgar +Franklin +Arthur +Lawrence +Dennis +Russell +Douglas +Leonard +Gregory +Harold +Frederick +Martin +Curtis +Stanley +Gilbert +Harvey +Francis +Eugene +Ralph +Roy +Albert +Bruce +Ronald +Keith +Craig +Roger +Randy +Gary +Dennis +Edwin +Don +Glen +Gordon +Howard +Earl +Leo +Lloyd +Milton +Norman +Roland +Vernon +Warren +Alfred +Bernard +Chester +Clarence +Clifford +Clyde +Dale +Dan +Darrell +Floyd +Herman +Jerome +Maurice +Neil +Ray +Rodney +Roland +Stuart +Wallace +Wayne +Wendell +Barry +Cecil +Claude +Daryl +Edmund +Everett +Ferdinand +Forrest +Gerald +Hugh +Irving +Leslie +Marvin +Morris +Nelson +Perry +Phillip +Roderick +Ross +Terrence +Wade +Winston +Zachariah diff --git a/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/en/lastnames b/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/en/lastnames index 43ef2f61a..1e007ecc9 100644 --- a/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/en/lastnames +++ b/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/en/lastnames @@ -165,3 +165,107 @@ Shaw Snyder Mason Dixon +Blackwood +Shepherd +Frost +Hawkins +Pearson +Fleming +Dawson +Palmer +Nash +Barker +Thornton +Fitzgerald +Winters +Mckenzie +Chandler +Griffith +Cunningham +Doyle +Fletcher +Hicks +Walton +Briggs +Pearce +Nichols +Blake +Hodges +Benson +Marsh +Whitaker +Skinner +Robbins +Goodwin +Kirby +Savage +Hensley +Hancock +Pratt +Gallagher +Yates +Dennis +Swanson +Steele +Bauer +Holt +Barber +Schultz +Foley +Fowler +Wise +Malone +Cannon +Tate +Stark +Welch +Dyer +Booth +Payne +Shannon +Harmon +Woodward +Morse +Jacobson +Knowles +Blanchard +Dillon +Stokes +Buckley +Dickerson +Middleton +Sellers +Cobb +Stephenson +Roach +Moody +Beard +Mccarthy +Garner +Mcguire +Sloan +Ballard +Shields +Orr +Savage +Graves +Dempsey +Weeks +Mckay +Cooke +Riddle +Gates +Atkins +Farrell +Lowery +Huffman +Livingston +Davenport +Hendricks +Kerr +Pollard +Hoover +Wolfe +Bowman +Underwood +Frazier diff --git a/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/nl/firstnames_female b/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/nl/firstnames_female index b722363f6..265cef9a5 100644 --- a/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/nl/firstnames_female +++ b/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/nl/firstnames_female @@ -102,3 +102,110 @@ Juul Lise Myrthe Veerle +Aafke +Alicia +Amira +Aniek +Annabel +Annelies +Anouk +Astrid +Babette +Bianca +Britt +Carlijn +Chantal +Claire +Dagmar +Danique +Daphne +Denise +Dominique +Doris +Eefje +Elena +Eline +Elisa +Elisabeth +Ellen +Esther +Eveline +Fabienne +Felice +Fleur +Frederique +Gwen +Hanna +Heleen +Helena +Ilona +Imke +Inge +Irene +Iris +Janna +Janneke +Jasmine +Jennifer +Jessica +Joelle +Judith +Julia +Karin +Karlijn +Kim +Kirsten +Kyra +Laura +Lena +Lianne +Liesbeth +Linda +Lisanne +Lisette +Louise +Maartje +Manon +Margot +Marieke +Marijke +Marlies +Marloes +Marthe +Melissa +Michelle +Nadine +Natalie +Nicole +Nina +Noortje +Paulien +Petra +Rachel +Renee +Robin +Rosa +Roxanne +Sabine +Sandra +Saskia +Silke +Simone +Suzanne +Sylvie +Tamara +Tanja +Tara +Thea +Thirza +Tina +Tineke +Ursula +Victoria +Wendy +Wilma +Xandra +Yasmin +Yvette +Yvonne +Zara diff --git a/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/nl/firstnames_male b/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/nl/firstnames_male index 2420060f1..288c81a86 100644 --- a/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/nl/firstnames_male +++ b/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/nl/firstnames_male @@ -99,3 +99,114 @@ Mijs Mika Felix Merlijn +Alexander +Aron +Arthur +Axel +Bas +Bastiaan +Berend +Björn +Casper +Cees +Chris +Christian +Christiaan +Colin +Cornelis +Dani +Dennis +Dirk +Dominic +Eduard +Eelco +Erik +Erwin +Ezra +Faas +Filip +Florian +Frank +Frederik +Freek +Gerard +Gerrit +Giel +Gijs +Glenn +Govert +Harm +Harold +Hendrik +Henrik +Huub +Ian +Ivo +Jacob +Jake +Jan +Jarno +Jason +Jeffrey +Jeremy +Jim +Jimmy +Johan +Johannes +Jonas +Jonathan +Jos +Joshua +Justin +Kay +Kevin +Kjeld +Klaas +Lennard +Lennart +Leon +Lex +Liam +Loek +Lorenzo +Louis +Lowie +Maarten +Magnus +Maikel +Marc +Marcel +Marco +Martijn +Mathias +Matthijs +Maurits +Menno +Michiel +Nathan +Nico +Oscar +Pascal +Patrick +Paul +Peter +Philip +Pieter +Pim +Quincy +Remco +Rick +Rik +Robert +Rogier +Rowan +Ruud +Simon +Stefan +Steven +Thom +Victor +Vincent +Willem +Wouter +Yannick diff --git a/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/nl/lastnames b/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/nl/lastnames index 21d1b590c..5769c4ffd 100644 --- a/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/nl/lastnames +++ b/src/Generators/AliasVault.Generators.Identity/Implementations/Dictionaries/nl/lastnames @@ -104,3 +104,104 @@ van Asselt Timmermans van Vliet van Rijn +van Schaik +Bosman +Wolters +van Hout +Hermans +van Rooij +de Vos +van Donselaar +Evers +van den Brink +Verkerk +Groeneveld +van Duijn +Schuurman +Hoogendoorn +van Zanten +Koopman +Cornelissen +van Driel +Teunissen +Versteeg +van Deursen +Schipper +van Kempen +Bouwman +van der Valk +Nijhuis +van der Werf +van den Akker +Verhoef +Wessels +van der Poel +Driessen +van Oosten +Lambrechts +van der Vlist +Hoogeveen +van Gils +Rietveld +Barendrecht +van der Spek +Stam +van der Linde +Boersma +van Dijk +Schepers +van der Kolk +Roelofs +van der Velden +van den Burg +Westra +van der Steen +Pronk +van der Veer +Rozendaal +van den Bos +Konings +van der Wiel +Noordam +van der Laan +Schut +van der Vlugt +Witteveen +van der Zwan +Boogaard +van der Waal +Stolk +van der Windt +Rutten +van der Zanden +Spaans +van der Zwaan +Roos +van der Zijl +Schoenmaker +van Diepen +Romeijn +van Doesburg +Schippers +van Eck +Rijken +van Egmond +Schrama +van Eijk +Ruijter +van Engelen +Sanders +van Es +Schenk +van Essen +van Gaal +van Geenen +van Gent +van Gestel +van Gool +van Grinsven +van Gurp +van Haaften +van Haren +van Hattem +van Hees diff --git a/src/Generators/AliasVault.Generators.Identity/UsernameEmailGenerator.cs b/src/Generators/AliasVault.Generators.Identity/UsernameEmailGenerator.cs index 2979304be..83fa70748 100644 --- a/src/Generators/AliasVault.Generators.Identity/UsernameEmailGenerator.cs +++ b/src/Generators/AliasVault.Generators.Identity/UsernameEmailGenerator.cs @@ -67,25 +67,55 @@ public class UsernameEmailGenerator { var parts = new List(); - // Use first initial + last name - if (_random.Next(2) == 0) + switch (_random.Next(4)) { - parts.Add(identity.FirstName.Substring(0, 1).ToLower() + identity.LastName.ToLower()); - } - else - { - // Use full name - parts.Add((identity.FirstName + identity.LastName).ToLower()); + case 0: + // First initial + last name + parts.Add(identity.FirstName.Substring(0, 1).ToLower() + identity.LastName.ToLower()); + break; + case 1: + // Full name + parts.Add((identity.FirstName + identity.LastName).ToLower()); + break; + case 2: + // First name + last initial + parts.Add(identity.FirstName.ToLower() + identity.LastName.Substring(0, 1).ToLower()); + break; + case 3: + // First 3 chars of first name + last name + parts.Add(identity.FirstName.Substring(0, Math.Min(3, identity.FirstName.Length)).ToLower() + identity.LastName.ToLower()); + break; } - // Add birth year - if (_random.Next(2) == 0) + // Add birth year variations + if (_random.Next(3) != 0) { - parts.Add(identity.BirthDate.Year.ToString().Substring(2)); + switch (_random.Next(2)) + { + case 0: + parts.Add(identity.BirthDate.Year.ToString().Substring(2)); + break; + case 1: + parts.Add(identity.BirthDate.Year.ToString()); + break; + } + } + else if (_random.Next(2) == 0) + { + // Add random numbers for more uniqueness + parts.Add(_random.Next(10, 999).ToString()); } - // Join parts and sanitize + // Join parts with random symbols, possibly multiple var emailPrefix = string.Join(GetRandomSymbol(), parts); + + // Add extra random symbol at random position + if (_random.Next(2) == 0) + { + int position = _random.Next(emailPrefix.Length); + emailPrefix = emailPrefix.Insert(position, GetRandomSymbol()); + } + emailPrefix = SanitizeEmailPrefix(emailPrefix); // Adjust length From e4f2ca630b7c0fd564d58a8bf4c938ce66040948 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sun, 12 Jan 2025 14:01:48 +0000 Subject: [PATCH 2/3] Add server side email prefix generation method (#525) --- src/AliasVault.Api/AliasVault.Api.csproj | 1 + .../Controllers/IdentityController.cs | 163 ++++++++++++++++++ .../Controllers/VaultController.cs | 2 +- src/AliasVault.Api/Helpers/EmailHelper.cs | 24 +++ 4 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 src/AliasVault.Api/Controllers/IdentityController.cs create mode 100644 src/AliasVault.Api/Helpers/EmailHelper.cs diff --git a/src/AliasVault.Api/AliasVault.Api.csproj b/src/AliasVault.Api/AliasVault.Api.csproj index 193138a50..9aeb60f88 100644 --- a/src/AliasVault.Api/AliasVault.Api.csproj +++ b/src/AliasVault.Api/AliasVault.Api.csproj @@ -34,6 +34,7 @@ + diff --git a/src/AliasVault.Api/Controllers/IdentityController.cs b/src/AliasVault.Api/Controllers/IdentityController.cs new file mode 100644 index 000000000..c21e31edf --- /dev/null +++ b/src/AliasVault.Api/Controllers/IdentityController.cs @@ -0,0 +1,163 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.Api.Controllers; + +using AliasServerDb; +using AliasVault.Api.Controllers.Abstracts; +using AliasVault.Generators.Identity; +using AliasVault.Generators.Identity.Implementations.Factories; +using AliasVault.Generators.Identity.Models; +using Asp.Versioning; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +/// +/// Controller for generating identities taking into account existing information on the AliasVault server. +/// +/// UserManager instance. +/// DbContextFactory instance. +[ApiVersion("1")] +public class IdentityController(UserManager userManager, IAliasServerDbContextFactory dbContextFactory) : AuthenticatedRequestController(userManager) +{ + /// + /// Generates an email prefix based on provided identity fields. + /// + /// First name to use for generation. + /// Last name to use for generation. + /// Birth date to use for generation. + /// Gender to use for generation (Male/Female). + /// Email domain to use for checking if the to be generated email address is already taken. + /// Two letter language code (en/nl) to use for generation. + /// Generated email prefix. + [HttpGet("GenerateEmailPrefix")] + public async Task GenerateEmailPrefix( + string? firstName = null, + string? lastName = null, + DateTime? birthDate = null, + string? gender = null, + string? emailDomain = null, + string language = "en") + { + var user = await GetCurrentUserAsync(); + if (user == null) + { + return Unauthorized(); + } + + const int MaxAttempts = 10; + string emailPrefix; + var generator = new UsernameEmailGenerator(); + + // If no identity information is provided, generate a complete random identity + if (string.IsNullOrEmpty(firstName) && string.IsNullOrEmpty(lastName) && !birthDate.HasValue) + { + var identityGenerator = IdentityGeneratorFactory.CreateIdentityGenerator(language); + var identity = await identityGenerator.GenerateRandomIdentityAsync(); + emailPrefix = identity.EmailPrefix; + + // Try up to 10 times to generate a unique email prefix + int attempts = 1; + while (await EmailClaimExistsAsync(emailPrefix, emailDomain) && attempts < MaxAttempts) + { + identity = await identityGenerator.GenerateRandomIdentityAsync(); + emailPrefix = identity.EmailPrefix; + attempts++; + } + + // If still not unique, try with random numbers + if (await EmailClaimExistsAsync(emailPrefix, emailDomain)) + { + emailPrefix = await GenerateUniqueEmailPrefixWithNumbersAsync(emailPrefix, emailDomain); + } + + return Ok(new { emailPrefix }); + } + + // Create identity model with provided values + var identityModel = new Identity + { + FirstName = firstName ?? string.Empty, + LastName = lastName ?? string.Empty, + BirthDate = birthDate ?? DateTime.UtcNow.AddYears(-30), + Gender = gender?.Equals("Female", StringComparison.OrdinalIgnoreCase) == true ? Gender.Female : Gender.Male, + NickName = string.Empty, + }; + + // Generate initial email prefix + emailPrefix = generator.GenerateEmailPrefix(identityModel); + + // Try up to 10 times to generate a unique email prefix + int baseAttempts = 1; + while (await EmailClaimExistsAsync(emailPrefix, emailDomain) && baseAttempts < MaxAttempts) + { + emailPrefix = generator.GenerateEmailPrefix(identityModel); + baseAttempts++; + } + + // If still not unique, try with random numbers + if (await EmailClaimExistsAsync(emailPrefix, emailDomain)) + { + emailPrefix = await GenerateUniqueEmailPrefixWithNumbersAsync(emailPrefix); + } + + return Ok(new { emailPrefix }); + } + + /// + /// Verify that provided email address is not already taken by another user. + /// + /// The email prefix to check. + /// Email domain to use for checking if the to be generated email address is already taken. + /// True if the email address is already taken, false otherwise. + private async Task EmailClaimExistsAsync(string emailPrefix, string? emailDomain = null) + { + if (emailDomain == null) + { + // If no email domain is provided, we assume a non-aliasvault address is used which cannot be taken. + return false; + } + + await using var context = await dbContextFactory.CreateDbContextAsync(); + + var email = emailPrefix + "@" + emailDomain; + var claimExists = await context.UserEmailClaims.FirstOrDefaultAsync(c => c.Address == email); + + return claimExists != null; + } + + /// + /// Generate a unique email prefix with random numbers. + /// + /// The base prefix to use for generation. + /// Email domain to use for checking if the to be generated email address is already taken. + /// Unique email prefix with random numbers. + private async Task GenerateUniqueEmailPrefixWithNumbersAsync(string basePrefix, string? emailDomain = null) + { + if (emailDomain == null) + { + // If no email domain is provided, we assume a non-aliasvault address is used which cannot be taken. + return basePrefix; + } + + const int MaxAttempts = 10; + var random = new Random(); + + for (int i = 0; i < MaxAttempts; i++) + { + string prefix = $"{random.Next(10, 100)}.{basePrefix}.{random.Next(10, 100)}"; + if (!await EmailClaimExistsAsync(prefix, emailDomain)) + { + return prefix; + } + } + + // If all attempts fail, return the last generated prefix + return $"{random.Next(10, 100)}.{basePrefix}.{random.Next(10, 100)}"; + } +} diff --git a/src/AliasVault.Api/Controllers/VaultController.cs b/src/AliasVault.Api/Controllers/VaultController.cs index 58f9877e6..213e8a436 100644 --- a/src/AliasVault.Api/Controllers/VaultController.cs +++ b/src/AliasVault.Api/Controllers/VaultController.cs @@ -386,7 +386,7 @@ public class VaultController(ILogger logger, IAliasServerDbCont foreach (var email in newEmailAddresses) { // Sanitize email address. - var sanitizedEmail = email.Trim().ToLower(); + var sanitizedEmail = EmailHelper.SanitizeEmail(email); // If email address is invalid according to the EmailAddressAttribute, skip it. if (!new EmailAddressAttribute().IsValid(sanitizedEmail)) diff --git a/src/AliasVault.Api/Helpers/EmailHelper.cs b/src/AliasVault.Api/Helpers/EmailHelper.cs new file mode 100644 index 000000000..e7406abf4 --- /dev/null +++ b/src/AliasVault.Api/Helpers/EmailHelper.cs @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.Api.Helpers; + +/// +/// EmailHelper class which contains helper methods for email. +/// +public static class EmailHelper +{ + /// + /// Sanitize email address by trimming and converting to lowercase. + /// + /// Email address to sanitize. + /// Sanitized email address. + public static string SanitizeEmail(string email) + { + return email.Trim().ToLower(); + } +} From 92904dcf5595f0348bb6a86287f9e88de499406f Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sun, 12 Jan 2025 15:29:15 +0000 Subject: [PATCH 3/3] Refactor email prefix exists check (#525) --- .../Controllers/IdentityController.cs | 129 ++---------------- .../Services/CredentialService.cs | 47 +++++-- 2 files changed, 47 insertions(+), 129 deletions(-) diff --git a/src/AliasVault.Api/Controllers/IdentityController.cs b/src/AliasVault.Api/Controllers/IdentityController.cs index c21e31edf..34e8eae18 100644 --- a/src/AliasVault.Api/Controllers/IdentityController.cs +++ b/src/AliasVault.Api/Controllers/IdentityController.cs @@ -9,9 +9,7 @@ namespace AliasVault.Api.Controllers; using AliasServerDb; using AliasVault.Api.Controllers.Abstracts; -using AliasVault.Generators.Identity; -using AliasVault.Generators.Identity.Implementations.Factories; -using AliasVault.Generators.Identity.Models; +using AliasVault.Api.Helpers; using Asp.Versioning; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; @@ -26,23 +24,12 @@ using Microsoft.EntityFrameworkCore; public class IdentityController(UserManager userManager, IAliasServerDbContextFactory dbContextFactory) : AuthenticatedRequestController(userManager) { /// - /// Generates an email prefix based on provided identity fields. + /// Verify that provided email address is not already taken by another user. /// - /// First name to use for generation. - /// Last name to use for generation. - /// Birth date to use for generation. - /// Gender to use for generation (Male/Female). - /// Email domain to use for checking if the to be generated email address is already taken. - /// Two letter language code (en/nl) to use for generation. - /// Generated email prefix. - [HttpGet("GenerateEmailPrefix")] - public async Task GenerateEmailPrefix( - string? firstName = null, - string? lastName = null, - DateTime? birthDate = null, - string? gender = null, - string? emailDomain = null, - string language = "en") + /// The full email address to check. + /// True if the email address is already taken, false otherwise. + [HttpPost("CheckEmail/{email}")] + public async Task CheckEmail(string email) { var user = await GetCurrentUserAsync(); if (user == null) @@ -50,114 +37,22 @@ public class IdentityController(UserManager userManager, IAliasS return Unauthorized(); } - const int MaxAttempts = 10; - string emailPrefix; - var generator = new UsernameEmailGenerator(); - - // If no identity information is provided, generate a complete random identity - if (string.IsNullOrEmpty(firstName) && string.IsNullOrEmpty(lastName) && !birthDate.HasValue) - { - var identityGenerator = IdentityGeneratorFactory.CreateIdentityGenerator(language); - var identity = await identityGenerator.GenerateRandomIdentityAsync(); - emailPrefix = identity.EmailPrefix; - - // Try up to 10 times to generate a unique email prefix - int attempts = 1; - while (await EmailClaimExistsAsync(emailPrefix, emailDomain) && attempts < MaxAttempts) - { - identity = await identityGenerator.GenerateRandomIdentityAsync(); - emailPrefix = identity.EmailPrefix; - attempts++; - } - - // If still not unique, try with random numbers - if (await EmailClaimExistsAsync(emailPrefix, emailDomain)) - { - emailPrefix = await GenerateUniqueEmailPrefixWithNumbersAsync(emailPrefix, emailDomain); - } - - return Ok(new { emailPrefix }); - } - - // Create identity model with provided values - var identityModel = new Identity - { - FirstName = firstName ?? string.Empty, - LastName = lastName ?? string.Empty, - BirthDate = birthDate ?? DateTime.UtcNow.AddYears(-30), - Gender = gender?.Equals("Female", StringComparison.OrdinalIgnoreCase) == true ? Gender.Female : Gender.Male, - NickName = string.Empty, - }; - - // Generate initial email prefix - emailPrefix = generator.GenerateEmailPrefix(identityModel); - - // Try up to 10 times to generate a unique email prefix - int baseAttempts = 1; - while (await EmailClaimExistsAsync(emailPrefix, emailDomain) && baseAttempts < MaxAttempts) - { - emailPrefix = generator.GenerateEmailPrefix(identityModel); - baseAttempts++; - } - - // If still not unique, try with random numbers - if (await EmailClaimExistsAsync(emailPrefix, emailDomain)) - { - emailPrefix = await GenerateUniqueEmailPrefixWithNumbersAsync(emailPrefix); - } - - return Ok(new { emailPrefix }); + bool isTaken = await EmailClaimExistsAsync(email); + return Ok(new { isTaken }); } /// /// Verify that provided email address is not already taken by another user. /// - /// The email prefix to check. - /// Email domain to use for checking if the to be generated email address is already taken. + /// The email address to check. /// True if the email address is already taken, false otherwise. - private async Task EmailClaimExistsAsync(string emailPrefix, string? emailDomain = null) + private async Task EmailClaimExistsAsync(string email) { - if (emailDomain == null) - { - // If no email domain is provided, we assume a non-aliasvault address is used which cannot be taken. - return false; - } - await using var context = await dbContextFactory.CreateDbContextAsync(); - var email = emailPrefix + "@" + emailDomain; - var claimExists = await context.UserEmailClaims.FirstOrDefaultAsync(c => c.Address == email); + var sanitizedEmail = EmailHelper.SanitizeEmail(email); + var claimExists = await context.UserEmailClaims.FirstOrDefaultAsync(c => c.Address == sanitizedEmail); return claimExists != null; } - - /// - /// Generate a unique email prefix with random numbers. - /// - /// The base prefix to use for generation. - /// Email domain to use for checking if the to be generated email address is already taken. - /// Unique email prefix with random numbers. - private async Task GenerateUniqueEmailPrefixWithNumbersAsync(string basePrefix, string? emailDomain = null) - { - if (emailDomain == null) - { - // If no email domain is provided, we assume a non-aliasvault address is used which cannot be taken. - return basePrefix; - } - - const int MaxAttempts = 10; - var random = new Random(); - - for (int i = 0; i < MaxAttempts; i++) - { - string prefix = $"{random.Next(10, 100)}.{basePrefix}.{random.Next(10, 100)}"; - if (!await EmailClaimExistsAsync(prefix, emailDomain)) - { - return prefix; - } - } - - // If all attempts fail, return the last generated prefix - return $"{random.Next(10, 100)}.{basePrefix}.{random.Next(10, 100)}"; - } } diff --git a/src/AliasVault.Client/Services/CredentialService.cs b/src/AliasVault.Client/Services/CredentialService.cs index a0525d7ea..a44904172 100644 --- a/src/AliasVault.Client/Services/CredentialService.cs +++ b/src/AliasVault.Client/Services/CredentialService.cs @@ -49,20 +49,43 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService /// Task. public async Task GenerateRandomIdentity(Credential credential) { - // Generate a random identity using the IIdentityGenerator implementation. - var identity = await IdentityGeneratorFactory.CreateIdentityGenerator(dbService.Settings.DefaultIdentityLanguage).GenerateRandomIdentityAsync(); + const int MaxAttempts = 5; + var attempts = 0; + bool isEmailTaken; - // Generate random values for the Identity properties - credential.Username = identity.NickName; - credential.Alias.FirstName = identity.FirstName; - credential.Alias.LastName = identity.LastName; - credential.Alias.NickName = identity.NickName; - credential.Alias.Gender = identity.Gender == Gender.Male ? "Male" : "Female"; - credential.Alias.BirthDate = identity.BirthDate; + do + { + // Generate a random identity using the IIdentityGenerator implementation + var identity = await IdentityGeneratorFactory.CreateIdentityGenerator(dbService.Settings.DefaultIdentityLanguage).GenerateRandomIdentityAsync(); - // Set the email - var emailDomain = GetDefaultEmailDomain(); - credential.Alias.Email = $"{identity.EmailPrefix}@{emailDomain}"; + // Generate random values for the Identity properties + credential.Username = identity.NickName; + credential.Alias.FirstName = identity.FirstName; + credential.Alias.LastName = identity.LastName; + credential.Alias.NickName = identity.NickName; + credential.Alias.Gender = identity.Gender == Gender.Male ? "Male" : "Female"; + credential.Alias.BirthDate = identity.BirthDate; + + // Set the email + var emailDomain = GetDefaultEmailDomain(); + credential.Alias.Email = $"{identity.EmailPrefix}@{emailDomain}"; + + // Check if email is already taken + try + { + var response = await httpClient.PostAsync($"v1/Identity/CheckEmail/{credential.Alias.Email}", null); + var result = await response.Content.ReadFromJsonAsync>(); + isEmailTaken = result?["isTaken"] ?? false; + } + catch + { + // If the API call fails, assume email is not taken to allow operation to continue + isEmailTaken = false; + } + + attempts++; + } + while (isEmailTaken && attempts < MaxAttempts); // Generate password credential.Passwords.First().Value = GenerateRandomPassword();