From cbe8b2c471b05bdcbb12ecf822d768bdf3ec3496 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 6 Jun 2025 13:19:52 +0200 Subject: [PATCH] Make shared generators work when called from .NET Blazor interop (#896) --- .../shared/identity-generator/index.d.ts | 75 ++++++++++--------- .../utils/shared/identity-generator/index.js | 26 +++++-- .../utils/shared/identity-generator/index.mjs | 25 +++++-- .../shared/identity-generator/index.d.ts | 75 ++++++++++--------- .../utils/shared/identity-generator/index.js | 26 +++++-- .../utils/shared/identity-generator/index.mjs | 25 +++++-- .../Widgets/CreateNewIdentityWidget.razor | 2 +- .../Main/Pages/Credentials/AddEdit.razor | 68 ++++++++++------- .../Services/CredentialService.cs | 6 +- .../Services/JsInteropService.cs | 70 +++++++++++++++-- .../js/shared/identity-generator/index.d.ts | 75 ++++++++++--------- .../js/shared/identity-generator/index.js | 26 +++++-- .../js/shared/identity-generator/index.mjs | 25 +++++-- .../UsernameEmailGeneratorFactory.ts | 10 +++ shared/identity-generator/src/index.ts | 3 +- .../src/utils/UsernameEmailGenerator.ts | 5 ++ 16 files changed, 354 insertions(+), 188 deletions(-) create mode 100644 shared/identity-generator/src/factories/UsernameEmailGeneratorFactory.ts diff --git a/apps/browser-extension/src/utils/shared/identity-generator/index.d.ts b/apps/browser-extension/src/utils/shared/identity-generator/index.d.ts index 88de04a9d..0bfea65ed 100644 --- a/apps/browser-extension/src/utils/shared/identity-generator/index.d.ts +++ b/apps/browser-extension/src/utils/shared/identity-generator/index.d.ts @@ -16,39 +16,6 @@ type Identity = { nickName: string; }; -/** - * Generate a username or email prefix. - */ -declare class UsernameEmailGenerator { - private static readonly MIN_LENGTH; - private static readonly MAX_LENGTH; - private readonly symbols; - /** - * Generate a username based on an identity. - */ - generateUsername(identity: Identity): string; - /** - * Generate an email prefix based on an identity. - */ - generateEmailPrefix(identity: Identity): string; - /** - * Sanitize an email prefix. - */ - private sanitizeEmailPrefix; - /** - * Get a random symbol. - */ - private getRandomSymbol; - /** - * Generate a random string. - */ - private generateRandomString; - /** - * Generate a secure random integer between 0 (inclusive) and max (exclusive) - */ - private getSecureRandom; -} - interface IIdentityGenerator { generateRandomIdentity(): Identity; } @@ -132,6 +99,39 @@ declare class IdentityHelperUtils { static isValidBirthDate(input: string | undefined): boolean; } +/** + * Generate a username or email prefix. + */ +declare class UsernameEmailGenerator { + private static readonly MIN_LENGTH; + private static readonly MAX_LENGTH; + private readonly symbols; + /** + * Generate a username based on an identity. + */ + generateUsername(identity: Identity): string; + /** + * Generate an email prefix based on an identity. + */ + generateEmailPrefix(identity: Identity): string; + /** + * Sanitize an email prefix. + */ + private sanitizeEmailPrefix; + /** + * Get a random symbol. + */ + private getRandomSymbol; + /** + * Generate a random string. + */ + private generateRandomString; + /** + * Generate a secure random integer between 0 (inclusive) and max (exclusive) + */ + private getSecureRandom; +} + /** * Creates a new identity generator based on the language. * @param language - The language to use for generating the identity (e.g. "en", "nl"). @@ -139,4 +139,11 @@ declare class IdentityHelperUtils { */ declare const CreateIdentityGenerator: (language: string) => IIdentityGenerator; -export { CreateIdentityGenerator, Gender, type Identity, IdentityGenerator, IdentityGeneratorEn, IdentityGeneratorNl, IdentityHelperUtils, UsernameEmailGenerator }; +/** + * Creates a new username email generator. This is used by the .NET Blazor WASM JSinterop + * as it cannot create instances of classes directly, it has to use a factory method. + * @returns A new username email generator instance. + */ +declare const CreateUsernameEmailGenerator: () => UsernameEmailGenerator; + +export { CreateIdentityGenerator, CreateUsernameEmailGenerator, Gender, type Identity, IdentityGenerator, IdentityGeneratorEn, IdentityGeneratorNl, IdentityHelperUtils, UsernameEmailGenerator }; diff --git a/apps/browser-extension/src/utils/shared/identity-generator/index.js b/apps/browser-extension/src/utils/shared/identity-generator/index.js index a67df55d1..839c41710 100644 --- a/apps/browser-extension/src/utils/shared/identity-generator/index.js +++ b/apps/browser-extension/src/utils/shared/identity-generator/index.js @@ -24,6 +24,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru var index_exports = {}; __export(index_exports, { CreateIdentityGenerator: () => CreateIdentityGenerator, + CreateUsernameEmailGenerator: () => CreateUsernameEmailGenerator, Gender: () => Gender, IdentityGenerator: () => IdentityGenerator, IdentityGeneratorEn: () => IdentityGeneratorEn, @@ -33,6 +34,14 @@ __export(index_exports, { }); module.exports = __toCommonJS(index_exports); +// src/types/Gender.ts +var Gender = /* @__PURE__ */ ((Gender2) => { + Gender2["Male"] = "Male"; + Gender2["Female"] = "Female"; + Gender2["Other"] = "Other"; + return Gender2; +})(Gender || {}); + // src/utils/UsernameEmailGenerator.ts var _UsernameEmailGenerator = class _UsernameEmailGenerator { constructor() { @@ -56,6 +65,9 @@ var _UsernameEmailGenerator = class _UsernameEmailGenerator { */ generateEmailPrefix(identity) { const parts = []; + if (typeof identity.birthDate === "string") { + identity.birthDate = new Date(identity.birthDate); + } switch (this.getSecureRandom(4)) { case 0: parts.push(identity.firstName.substring(0, 1).toLowerCase() + identity.lastName.toLowerCase()); @@ -131,14 +143,6 @@ _UsernameEmailGenerator.MIN_LENGTH = 6; _UsernameEmailGenerator.MAX_LENGTH = 20; var UsernameEmailGenerator = _UsernameEmailGenerator; -// src/types/Gender.ts -var Gender = /* @__PURE__ */ ((Gender2) => { - Gender2["Male"] = "Male"; - Gender2["Female"] = "Female"; - Gender2["Other"] = "Other"; - return Gender2; -})(Gender || {}); - // src/implementations/base/IdentityGenerator.ts var IdentityGenerator = class { /** @@ -1727,9 +1731,15 @@ var CreateIdentityGenerator = (language) => { } throw new Error(`Unsupported language: ${language}`); }; + +// src/factories/UsernameEmailGeneratorFactory.ts +var CreateUsernameEmailGenerator = () => { + return new UsernameEmailGenerator(); +}; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { CreateIdentityGenerator, + CreateUsernameEmailGenerator, Gender, IdentityGenerator, IdentityGeneratorEn, diff --git a/apps/browser-extension/src/utils/shared/identity-generator/index.mjs b/apps/browser-extension/src/utils/shared/identity-generator/index.mjs index b77d669f6..32e92127c 100644 --- a/apps/browser-extension/src/utils/shared/identity-generator/index.mjs +++ b/apps/browser-extension/src/utils/shared/identity-generator/index.mjs @@ -2,6 +2,14 @@ // This file was automatically generated. Do not edit manually. +// src/types/Gender.ts +var Gender = /* @__PURE__ */ ((Gender2) => { + Gender2["Male"] = "Male"; + Gender2["Female"] = "Female"; + Gender2["Other"] = "Other"; + return Gender2; +})(Gender || {}); + // src/utils/UsernameEmailGenerator.ts var _UsernameEmailGenerator = class _UsernameEmailGenerator { constructor() { @@ -25,6 +33,9 @@ var _UsernameEmailGenerator = class _UsernameEmailGenerator { */ generateEmailPrefix(identity) { const parts = []; + if (typeof identity.birthDate === "string") { + identity.birthDate = new Date(identity.birthDate); + } switch (this.getSecureRandom(4)) { case 0: parts.push(identity.firstName.substring(0, 1).toLowerCase() + identity.lastName.toLowerCase()); @@ -100,14 +111,6 @@ _UsernameEmailGenerator.MIN_LENGTH = 6; _UsernameEmailGenerator.MAX_LENGTH = 20; var UsernameEmailGenerator = _UsernameEmailGenerator; -// src/types/Gender.ts -var Gender = /* @__PURE__ */ ((Gender2) => { - Gender2["Male"] = "Male"; - Gender2["Female"] = "Female"; - Gender2["Other"] = "Other"; - return Gender2; -})(Gender || {}); - // src/implementations/base/IdentityGenerator.ts var IdentityGenerator = class { /** @@ -1696,8 +1699,14 @@ var CreateIdentityGenerator = (language) => { } throw new Error(`Unsupported language: ${language}`); }; + +// src/factories/UsernameEmailGeneratorFactory.ts +var CreateUsernameEmailGenerator = () => { + return new UsernameEmailGenerator(); +}; export { CreateIdentityGenerator, + CreateUsernameEmailGenerator, Gender, IdentityGenerator, IdentityGeneratorEn, diff --git a/apps/mobile-app/utils/shared/identity-generator/index.d.ts b/apps/mobile-app/utils/shared/identity-generator/index.d.ts index 88de04a9d..0bfea65ed 100644 --- a/apps/mobile-app/utils/shared/identity-generator/index.d.ts +++ b/apps/mobile-app/utils/shared/identity-generator/index.d.ts @@ -16,39 +16,6 @@ type Identity = { nickName: string; }; -/** - * Generate a username or email prefix. - */ -declare class UsernameEmailGenerator { - private static readonly MIN_LENGTH; - private static readonly MAX_LENGTH; - private readonly symbols; - /** - * Generate a username based on an identity. - */ - generateUsername(identity: Identity): string; - /** - * Generate an email prefix based on an identity. - */ - generateEmailPrefix(identity: Identity): string; - /** - * Sanitize an email prefix. - */ - private sanitizeEmailPrefix; - /** - * Get a random symbol. - */ - private getRandomSymbol; - /** - * Generate a random string. - */ - private generateRandomString; - /** - * Generate a secure random integer between 0 (inclusive) and max (exclusive) - */ - private getSecureRandom; -} - interface IIdentityGenerator { generateRandomIdentity(): Identity; } @@ -132,6 +99,39 @@ declare class IdentityHelperUtils { static isValidBirthDate(input: string | undefined): boolean; } +/** + * Generate a username or email prefix. + */ +declare class UsernameEmailGenerator { + private static readonly MIN_LENGTH; + private static readonly MAX_LENGTH; + private readonly symbols; + /** + * Generate a username based on an identity. + */ + generateUsername(identity: Identity): string; + /** + * Generate an email prefix based on an identity. + */ + generateEmailPrefix(identity: Identity): string; + /** + * Sanitize an email prefix. + */ + private sanitizeEmailPrefix; + /** + * Get a random symbol. + */ + private getRandomSymbol; + /** + * Generate a random string. + */ + private generateRandomString; + /** + * Generate a secure random integer between 0 (inclusive) and max (exclusive) + */ + private getSecureRandom; +} + /** * Creates a new identity generator based on the language. * @param language - The language to use for generating the identity (e.g. "en", "nl"). @@ -139,4 +139,11 @@ declare class IdentityHelperUtils { */ declare const CreateIdentityGenerator: (language: string) => IIdentityGenerator; -export { CreateIdentityGenerator, Gender, type Identity, IdentityGenerator, IdentityGeneratorEn, IdentityGeneratorNl, IdentityHelperUtils, UsernameEmailGenerator }; +/** + * Creates a new username email generator. This is used by the .NET Blazor WASM JSinterop + * as it cannot create instances of classes directly, it has to use a factory method. + * @returns A new username email generator instance. + */ +declare const CreateUsernameEmailGenerator: () => UsernameEmailGenerator; + +export { CreateIdentityGenerator, CreateUsernameEmailGenerator, Gender, type Identity, IdentityGenerator, IdentityGeneratorEn, IdentityGeneratorNl, IdentityHelperUtils, UsernameEmailGenerator }; diff --git a/apps/mobile-app/utils/shared/identity-generator/index.js b/apps/mobile-app/utils/shared/identity-generator/index.js index a67df55d1..839c41710 100644 --- a/apps/mobile-app/utils/shared/identity-generator/index.js +++ b/apps/mobile-app/utils/shared/identity-generator/index.js @@ -24,6 +24,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru var index_exports = {}; __export(index_exports, { CreateIdentityGenerator: () => CreateIdentityGenerator, + CreateUsernameEmailGenerator: () => CreateUsernameEmailGenerator, Gender: () => Gender, IdentityGenerator: () => IdentityGenerator, IdentityGeneratorEn: () => IdentityGeneratorEn, @@ -33,6 +34,14 @@ __export(index_exports, { }); module.exports = __toCommonJS(index_exports); +// src/types/Gender.ts +var Gender = /* @__PURE__ */ ((Gender2) => { + Gender2["Male"] = "Male"; + Gender2["Female"] = "Female"; + Gender2["Other"] = "Other"; + return Gender2; +})(Gender || {}); + // src/utils/UsernameEmailGenerator.ts var _UsernameEmailGenerator = class _UsernameEmailGenerator { constructor() { @@ -56,6 +65,9 @@ var _UsernameEmailGenerator = class _UsernameEmailGenerator { */ generateEmailPrefix(identity) { const parts = []; + if (typeof identity.birthDate === "string") { + identity.birthDate = new Date(identity.birthDate); + } switch (this.getSecureRandom(4)) { case 0: parts.push(identity.firstName.substring(0, 1).toLowerCase() + identity.lastName.toLowerCase()); @@ -131,14 +143,6 @@ _UsernameEmailGenerator.MIN_LENGTH = 6; _UsernameEmailGenerator.MAX_LENGTH = 20; var UsernameEmailGenerator = _UsernameEmailGenerator; -// src/types/Gender.ts -var Gender = /* @__PURE__ */ ((Gender2) => { - Gender2["Male"] = "Male"; - Gender2["Female"] = "Female"; - Gender2["Other"] = "Other"; - return Gender2; -})(Gender || {}); - // src/implementations/base/IdentityGenerator.ts var IdentityGenerator = class { /** @@ -1727,9 +1731,15 @@ var CreateIdentityGenerator = (language) => { } throw new Error(`Unsupported language: ${language}`); }; + +// src/factories/UsernameEmailGeneratorFactory.ts +var CreateUsernameEmailGenerator = () => { + return new UsernameEmailGenerator(); +}; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { CreateIdentityGenerator, + CreateUsernameEmailGenerator, Gender, IdentityGenerator, IdentityGeneratorEn, diff --git a/apps/mobile-app/utils/shared/identity-generator/index.mjs b/apps/mobile-app/utils/shared/identity-generator/index.mjs index b77d669f6..32e92127c 100644 --- a/apps/mobile-app/utils/shared/identity-generator/index.mjs +++ b/apps/mobile-app/utils/shared/identity-generator/index.mjs @@ -2,6 +2,14 @@ // This file was automatically generated. Do not edit manually. +// src/types/Gender.ts +var Gender = /* @__PURE__ */ ((Gender2) => { + Gender2["Male"] = "Male"; + Gender2["Female"] = "Female"; + Gender2["Other"] = "Other"; + return Gender2; +})(Gender || {}); + // src/utils/UsernameEmailGenerator.ts var _UsernameEmailGenerator = class _UsernameEmailGenerator { constructor() { @@ -25,6 +33,9 @@ var _UsernameEmailGenerator = class _UsernameEmailGenerator { */ generateEmailPrefix(identity) { const parts = []; + if (typeof identity.birthDate === "string") { + identity.birthDate = new Date(identity.birthDate); + } switch (this.getSecureRandom(4)) { case 0: parts.push(identity.firstName.substring(0, 1).toLowerCase() + identity.lastName.toLowerCase()); @@ -100,14 +111,6 @@ _UsernameEmailGenerator.MIN_LENGTH = 6; _UsernameEmailGenerator.MAX_LENGTH = 20; var UsernameEmailGenerator = _UsernameEmailGenerator; -// src/types/Gender.ts -var Gender = /* @__PURE__ */ ((Gender2) => { - Gender2["Male"] = "Male"; - Gender2["Female"] = "Female"; - Gender2["Other"] = "Other"; - return Gender2; -})(Gender || {}); - // src/implementations/base/IdentityGenerator.ts var IdentityGenerator = class { /** @@ -1696,8 +1699,14 @@ var CreateIdentityGenerator = (language) => { } throw new Error(`Unsupported language: ${language}`); }; + +// src/factories/UsernameEmailGeneratorFactory.ts +var CreateUsernameEmailGenerator = () => { + return new UsernameEmailGenerator(); +}; export { CreateIdentityGenerator, + CreateUsernameEmailGenerator, Gender, IdentityGenerator, IdentityGeneratorEn, diff --git a/apps/server/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor b/apps/server/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor index bd4bdad6c..f236168a8 100644 --- a/apps/server/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor +++ b/apps/server/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor @@ -161,7 +161,7 @@ } credential.Passwords = new List { new() }; - await CredentialService.GenerateRandomIdentity(credential); + await CredentialService.GenerateRandomIdentityAsync(credential); var id = await CredentialService.InsertEntryAsync(credential); if (id == Guid.Empty) diff --git a/apps/server/AliasVault.Client/Main/Pages/Credentials/AddEdit.razor b/apps/server/AliasVault.Client/Main/Pages/Credentials/AddEdit.razor index 61961f8e9..3239b621c 100644 --- a/apps/server/AliasVault.Client/Main/Pages/Credentials/AddEdit.razor +++ b/apps/server/AliasVault.Client/Main/Pages/Credentials/AddEdit.razor @@ -2,12 +2,7 @@ @page "/credentials/{id:guid}/edit" @inherits MainBase @inject CredentialService CredentialService -@using System.Globalization -@using System.Text.Json -@using System.Text.Json.Serialization -@using AliasVault.Generators.Identity -@using AliasVault.Generators.Identity.Implementations.Factories -@using AliasVault.Generators.Identity.Models +@using AliasVault.Client.Services @inject IJSRuntime JSRuntime @implements IAsyncDisposable @@ -192,7 +187,7 @@ else if (firstRender) { Module = await JSRuntime.InvokeAsync("import", "./js/modules/newIdentityWidget.js"); - + if (EditMode) { await LoadExistingCredential(); @@ -233,7 +228,7 @@ else } Obj = CredentialEdit.FromEntity(alias); - + // If BirthDate is MinValue, set AliasBirthDate to empty string // TODO: after date field in alias data model is made optional and // all min values have been replaced with null, we can remove this check. @@ -241,33 +236,39 @@ else { Obj.AliasBirthDate = string.Empty; } - + if (Obj.ServiceUrl is null) { Obj.ServiceUrl = CredentialService.DefaultServiceUrl; } } + /// + /// Creates a new credential object. + /// + private Credential CreateNewCredentialObject() + { + var credential = new Credential(); + credential.Alias = new Alias(); + credential.Alias.Email = "@" + CredentialService.GetDefaultEmailDomain(); + credential.Service = new Service(); + credential.Passwords = new List { new Password() }; + credential.TotpCodes = new List(); + + return credential; + } + /// /// Creates a new credential object. /// private void CreateNewCredential() { - // Create new Obj - var alias = new Credential(); - alias.Alias = new Alias(); - alias.Alias.Email = "@" + CredentialService.GetDefaultEmailDomain(); - alias.Service = new Service(); - alias.Passwords = new List { new Password() }; - alias.TotpCodes = new List(); + Obj = CredentialEdit.FromEntity(CreateNewCredentialObject()); - Obj = CredentialEdit.FromEntity(alias); - // Always set AliasBirthDate to empty for new credentials // TODO: after date field in alias data model is made optional and // all min values have been replaced with null, we can remove this check. Obj.AliasBirthDate = string.Empty; - Obj.ServiceUrl = CredentialService.DefaultServiceUrl; } @@ -321,8 +322,8 @@ else string currentPassword = Obj.Password.Value ?? string.Empty; // Generate random identity but preserve username and password - Obj = CredentialEdit.FromEntity(await CredentialService.GenerateRandomIdentity(Obj.ToEntity())); - + Obj = CredentialEdit.FromEntity(await CredentialService.GenerateRandomIdentityAsync(Obj.ToEntity())); + // Restore username and password Obj.Username = currentUsername; Obj.Password.Value = currentPassword; @@ -330,7 +331,7 @@ else else { // For new credentials, generate everything - Obj = CredentialEdit.FromEntity(await CredentialService.GenerateRandomIdentity(Obj.ToEntity())); + Obj = CredentialEdit.FromEntity(await CredentialService.GenerateRandomIdentityAsync(Obj.ToEntity())); IsPasswordVisible = true; } @@ -345,26 +346,35 @@ else private async Task GenerateRandomUsername() { // If current object is null, then we create a new random identity. - Identity identity; + JsInteropService.AliasVaultIdentity identity; if (Obj.Alias.FirstName is null && Obj.Alias.LastName is null && Obj.Alias.BirthDate == DateTime.MinValue) { - identity = await IdentityGeneratorFactory.CreateIdentityGenerator(DbService.Settings.DefaultIdentityLanguage).GenerateRandomIdentityAsync(); + // Create new Credential object to avoid modifying the original object + var randomIdentity = await CredentialService.GenerateRandomIdentityAsync(CreateNewCredentialObject()); + + identity = new JsInteropService.AliasVaultIdentity + { + FirstName = randomIdentity.Alias.FirstName ?? string.Empty, + LastName = randomIdentity.Alias.LastName ?? string.Empty, + BirthDate = randomIdentity.Alias.BirthDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + Gender = randomIdentity.Alias.Gender, + NickName = randomIdentity.Alias.NickName ?? string.Empty, + }; } else { // Assemble identity model with the current values - identity = new Identity + identity = new JsInteropService.AliasVaultIdentity { FirstName = Obj.Alias.FirstName ?? string.Empty, LastName = Obj.Alias.LastName ?? string.Empty, - BirthDate = Obj.Alias.BirthDate, - Gender = Obj.Alias.Gender == Gender.Female.ToString() ? Gender.Female : Gender.Male, + BirthDate = Obj.Alias.BirthDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + Gender = Obj.Alias.Gender, NickName = Obj.Alias.NickName ?? string.Empty, }; } - var generator = new UsernameEmailGenerator(); - Obj.Username = generator.GenerateUsername(identity); + Obj.Username = await JsInteropService.GenerateRandomUsernameAsync(identity); } /// diff --git a/apps/server/AliasVault.Client/Services/CredentialService.cs b/apps/server/AliasVault.Client/Services/CredentialService.cs index 4ff9325d1..d79a74a39 100644 --- a/apps/server/AliasVault.Client/Services/CredentialService.cs +++ b/apps/server/AliasVault.Client/Services/CredentialService.cs @@ -49,7 +49,7 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService /// /// The credential object to update. /// Task. - public async Task GenerateRandomIdentity(Credential credential) + public async Task GenerateRandomIdentityAsync(Credential credential) { const int MaxAttempts = 5; var attempts = 0; @@ -60,13 +60,15 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService // Generate a random identity using the TypeScript library var identity = await jsInteropService.GenerateRandomIdentityAsync(dbService.Settings.DefaultIdentityLanguage); + Console.WriteLine($"Generated identity: {identity.FirstName} {identity.LastName} {identity.BirthDate} {identity.Gender} {identity.NickName}"); + // 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; - credential.Alias.BirthDate = identity.BirthDate; + credential.Alias.BirthDate = string.IsNullOrEmpty(identity.BirthDate) ? DateTime.MinValue : DateTime.Parse(identity.BirthDate); // Set the email var emailDomain = GetDefaultEmailDomain(); diff --git a/apps/server/AliasVault.Client/Services/JsInteropService.cs b/apps/server/AliasVault.Client/Services/JsInteropService.cs index b8ca541c2..5c551329c 100644 --- a/apps/server/AliasVault.Client/Services/JsInteropService.cs +++ b/apps/server/AliasVault.Client/Services/JsInteropService.cs @@ -291,8 +291,8 @@ public sealed class JsInteropService(IJSRuntime jsRuntime) /// Generates a random identity using the specified language. /// /// The language to use for generating the identity (e.g. "en", "nl"). - /// A containing the generated identity information. - public async Task GenerateRandomIdentityAsync(string language) + /// A containing the generated identity information. + public async Task GenerateRandomIdentityAsync(string language) { try { @@ -306,7 +306,7 @@ public sealed class JsInteropService(IJSRuntime jsRuntime) } var generatorInstance = await _identityGeneratorModule.InvokeAsync("CreateIdentityGenerator", language); - var result = await generatorInstance.InvokeAsync("generateRandomIdentity"); + var result = await generatorInstance.InvokeAsync("generateRandomIdentity"); return result; } @@ -317,6 +317,66 @@ public sealed class JsInteropService(IJSRuntime jsRuntime) } } + /// + /// Generates a random username. + /// + /// The identity to use for generating the username. + /// The generated username. + public async Task GenerateRandomUsernameAsync(AliasVaultIdentity identity) + { + try + { + if (_identityGeneratorModule == null) + { + await InitializeAsync(); + if (_identityGeneratorModule == null) + { + throw new InvalidOperationException("Failed to initialize identity generator module"); + } + } + + Console.WriteLine($"Generating username for identity: {identity.FirstName} {identity.LastName} {identity.BirthDate} {identity.Gender} {identity.NickName}"); + + var generatorInstance = await _identityGeneratorModule.InvokeAsync("CreateUsernameEmailGenerator"); + var result = await generatorInstance.InvokeAsync("generateUsername", identity); + return result; + } + catch (JSException ex) + { + await Console.Error.WriteLineAsync($"JavaScript error generating username: {ex.Message}"); + throw new InvalidOperationException("Failed to generate random username", ex); + } + } + + /// + /// Generates a random email prefix. + /// + /// The identity to use for generating the email prefix. + /// The generated email prefix. + public async Task GenerateRandomEmailPrefixAsync(AliasVaultIdentity identity) + { + try + { + if (_identityGeneratorModule == null) + { + await InitializeAsync(); + if (_identityGeneratorModule == null) + { + throw new InvalidOperationException("Failed to initialize identity generator module"); + } + } + + var generatorInstance = await _identityGeneratorModule.InvokeAsync("CreateUsernameEmailGenerator"); + var result = await generatorInstance.InvokeAsync("generateEmailPrefix", identity); + return result; + } + catch (JSException ex) + { + await Console.Error.WriteLineAsync($"JavaScript error generating email prefix: {ex.Message}"); + throw new InvalidOperationException("Failed to generate random email prefix", ex); + } + } + /// /// Generates a random password using the specified settings. /// @@ -351,7 +411,7 @@ public sealed class JsInteropService(IJSRuntime jsRuntime) /// /// Represents the result of a JavaScript identity generator operation. /// - public sealed class IdentityGeneratorResult + public sealed class AliasVaultIdentity { /// /// Gets the first name. @@ -366,7 +426,7 @@ public sealed class JsInteropService(IJSRuntime jsRuntime) /// /// Gets the birth date. /// - public DateTime BirthDate { get; init; } + public string? BirthDate { get; init; } /// /// Gets the email prefix. diff --git a/apps/server/AliasVault.Client/wwwroot/js/shared/identity-generator/index.d.ts b/apps/server/AliasVault.Client/wwwroot/js/shared/identity-generator/index.d.ts index 88de04a9d..0bfea65ed 100644 --- a/apps/server/AliasVault.Client/wwwroot/js/shared/identity-generator/index.d.ts +++ b/apps/server/AliasVault.Client/wwwroot/js/shared/identity-generator/index.d.ts @@ -16,39 +16,6 @@ type Identity = { nickName: string; }; -/** - * Generate a username or email prefix. - */ -declare class UsernameEmailGenerator { - private static readonly MIN_LENGTH; - private static readonly MAX_LENGTH; - private readonly symbols; - /** - * Generate a username based on an identity. - */ - generateUsername(identity: Identity): string; - /** - * Generate an email prefix based on an identity. - */ - generateEmailPrefix(identity: Identity): string; - /** - * Sanitize an email prefix. - */ - private sanitizeEmailPrefix; - /** - * Get a random symbol. - */ - private getRandomSymbol; - /** - * Generate a random string. - */ - private generateRandomString; - /** - * Generate a secure random integer between 0 (inclusive) and max (exclusive) - */ - private getSecureRandom; -} - interface IIdentityGenerator { generateRandomIdentity(): Identity; } @@ -132,6 +99,39 @@ declare class IdentityHelperUtils { static isValidBirthDate(input: string | undefined): boolean; } +/** + * Generate a username or email prefix. + */ +declare class UsernameEmailGenerator { + private static readonly MIN_LENGTH; + private static readonly MAX_LENGTH; + private readonly symbols; + /** + * Generate a username based on an identity. + */ + generateUsername(identity: Identity): string; + /** + * Generate an email prefix based on an identity. + */ + generateEmailPrefix(identity: Identity): string; + /** + * Sanitize an email prefix. + */ + private sanitizeEmailPrefix; + /** + * Get a random symbol. + */ + private getRandomSymbol; + /** + * Generate a random string. + */ + private generateRandomString; + /** + * Generate a secure random integer between 0 (inclusive) and max (exclusive) + */ + private getSecureRandom; +} + /** * Creates a new identity generator based on the language. * @param language - The language to use for generating the identity (e.g. "en", "nl"). @@ -139,4 +139,11 @@ declare class IdentityHelperUtils { */ declare const CreateIdentityGenerator: (language: string) => IIdentityGenerator; -export { CreateIdentityGenerator, Gender, type Identity, IdentityGenerator, IdentityGeneratorEn, IdentityGeneratorNl, IdentityHelperUtils, UsernameEmailGenerator }; +/** + * Creates a new username email generator. This is used by the .NET Blazor WASM JSinterop + * as it cannot create instances of classes directly, it has to use a factory method. + * @returns A new username email generator instance. + */ +declare const CreateUsernameEmailGenerator: () => UsernameEmailGenerator; + +export { CreateIdentityGenerator, CreateUsernameEmailGenerator, Gender, type Identity, IdentityGenerator, IdentityGeneratorEn, IdentityGeneratorNl, IdentityHelperUtils, UsernameEmailGenerator }; diff --git a/apps/server/AliasVault.Client/wwwroot/js/shared/identity-generator/index.js b/apps/server/AliasVault.Client/wwwroot/js/shared/identity-generator/index.js index a67df55d1..839c41710 100644 --- a/apps/server/AliasVault.Client/wwwroot/js/shared/identity-generator/index.js +++ b/apps/server/AliasVault.Client/wwwroot/js/shared/identity-generator/index.js @@ -24,6 +24,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru var index_exports = {}; __export(index_exports, { CreateIdentityGenerator: () => CreateIdentityGenerator, + CreateUsernameEmailGenerator: () => CreateUsernameEmailGenerator, Gender: () => Gender, IdentityGenerator: () => IdentityGenerator, IdentityGeneratorEn: () => IdentityGeneratorEn, @@ -33,6 +34,14 @@ __export(index_exports, { }); module.exports = __toCommonJS(index_exports); +// src/types/Gender.ts +var Gender = /* @__PURE__ */ ((Gender2) => { + Gender2["Male"] = "Male"; + Gender2["Female"] = "Female"; + Gender2["Other"] = "Other"; + return Gender2; +})(Gender || {}); + // src/utils/UsernameEmailGenerator.ts var _UsernameEmailGenerator = class _UsernameEmailGenerator { constructor() { @@ -56,6 +65,9 @@ var _UsernameEmailGenerator = class _UsernameEmailGenerator { */ generateEmailPrefix(identity) { const parts = []; + if (typeof identity.birthDate === "string") { + identity.birthDate = new Date(identity.birthDate); + } switch (this.getSecureRandom(4)) { case 0: parts.push(identity.firstName.substring(0, 1).toLowerCase() + identity.lastName.toLowerCase()); @@ -131,14 +143,6 @@ _UsernameEmailGenerator.MIN_LENGTH = 6; _UsernameEmailGenerator.MAX_LENGTH = 20; var UsernameEmailGenerator = _UsernameEmailGenerator; -// src/types/Gender.ts -var Gender = /* @__PURE__ */ ((Gender2) => { - Gender2["Male"] = "Male"; - Gender2["Female"] = "Female"; - Gender2["Other"] = "Other"; - return Gender2; -})(Gender || {}); - // src/implementations/base/IdentityGenerator.ts var IdentityGenerator = class { /** @@ -1727,9 +1731,15 @@ var CreateIdentityGenerator = (language) => { } throw new Error(`Unsupported language: ${language}`); }; + +// src/factories/UsernameEmailGeneratorFactory.ts +var CreateUsernameEmailGenerator = () => { + return new UsernameEmailGenerator(); +}; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { CreateIdentityGenerator, + CreateUsernameEmailGenerator, Gender, IdentityGenerator, IdentityGeneratorEn, diff --git a/apps/server/AliasVault.Client/wwwroot/js/shared/identity-generator/index.mjs b/apps/server/AliasVault.Client/wwwroot/js/shared/identity-generator/index.mjs index b77d669f6..32e92127c 100644 --- a/apps/server/AliasVault.Client/wwwroot/js/shared/identity-generator/index.mjs +++ b/apps/server/AliasVault.Client/wwwroot/js/shared/identity-generator/index.mjs @@ -2,6 +2,14 @@ // This file was automatically generated. Do not edit manually. +// src/types/Gender.ts +var Gender = /* @__PURE__ */ ((Gender2) => { + Gender2["Male"] = "Male"; + Gender2["Female"] = "Female"; + Gender2["Other"] = "Other"; + return Gender2; +})(Gender || {}); + // src/utils/UsernameEmailGenerator.ts var _UsernameEmailGenerator = class _UsernameEmailGenerator { constructor() { @@ -25,6 +33,9 @@ var _UsernameEmailGenerator = class _UsernameEmailGenerator { */ generateEmailPrefix(identity) { const parts = []; + if (typeof identity.birthDate === "string") { + identity.birthDate = new Date(identity.birthDate); + } switch (this.getSecureRandom(4)) { case 0: parts.push(identity.firstName.substring(0, 1).toLowerCase() + identity.lastName.toLowerCase()); @@ -100,14 +111,6 @@ _UsernameEmailGenerator.MIN_LENGTH = 6; _UsernameEmailGenerator.MAX_LENGTH = 20; var UsernameEmailGenerator = _UsernameEmailGenerator; -// src/types/Gender.ts -var Gender = /* @__PURE__ */ ((Gender2) => { - Gender2["Male"] = "Male"; - Gender2["Female"] = "Female"; - Gender2["Other"] = "Other"; - return Gender2; -})(Gender || {}); - // src/implementations/base/IdentityGenerator.ts var IdentityGenerator = class { /** @@ -1696,8 +1699,14 @@ var CreateIdentityGenerator = (language) => { } throw new Error(`Unsupported language: ${language}`); }; + +// src/factories/UsernameEmailGeneratorFactory.ts +var CreateUsernameEmailGenerator = () => { + return new UsernameEmailGenerator(); +}; export { CreateIdentityGenerator, + CreateUsernameEmailGenerator, Gender, IdentityGenerator, IdentityGeneratorEn, diff --git a/shared/identity-generator/src/factories/UsernameEmailGeneratorFactory.ts b/shared/identity-generator/src/factories/UsernameEmailGeneratorFactory.ts new file mode 100644 index 000000000..2f5ed83e1 --- /dev/null +++ b/shared/identity-generator/src/factories/UsernameEmailGeneratorFactory.ts @@ -0,0 +1,10 @@ +import { UsernameEmailGenerator } from "src/utils/UsernameEmailGenerator"; + +/** + * Creates a new username email generator. This is used by the .NET Blazor WASM JSinterop + * as it cannot create instances of classes directly, it has to use a factory method. + * @returns A new username email generator instance. + */ +export const CreateUsernameEmailGenerator = (): UsernameEmailGenerator => { + return new UsernameEmailGenerator(); +}; diff --git a/shared/identity-generator/src/index.ts b/shared/identity-generator/src/index.ts index 426201cf0..eb02bcf29 100644 --- a/shared/identity-generator/src/index.ts +++ b/shared/identity-generator/src/index.ts @@ -1,8 +1,9 @@ -export * from './utils/UsernameEmailGenerator'; export * from './types/Identity'; export * from './types/Gender'; export * from './implementations/IdentityGeneratorEn'; export * from './implementations/IdentityGeneratorNl'; export * from './implementations/base/IdentityGenerator'; export * from './utils/IdentityHelperUtils'; +export * from './utils/UsernameEmailGenerator'; export * from './factories/IdentityGeneratorFactory'; +export * from './factories/UsernameEmailGeneratorFactory'; diff --git a/shared/identity-generator/src/utils/UsernameEmailGenerator.ts b/shared/identity-generator/src/utils/UsernameEmailGenerator.ts index 7430179bd..681c97da3 100644 --- a/shared/identity-generator/src/utils/UsernameEmailGenerator.ts +++ b/shared/identity-generator/src/utils/UsernameEmailGenerator.ts @@ -32,6 +32,11 @@ export class UsernameEmailGenerator { public generateEmailPrefix(identity: Identity): string { const parts: string[] = []; + // Make sure the birth date is not a string which can happen when using the .NET Blazor WASM JSInterop. + if (typeof identity.birthDate === 'string') { + identity.birthDate = new Date(identity.birthDate); + } + switch (this.getSecureRandom(4)) { case 0: // First initial + last name