diff --git a/util/Seeder/IQuery.cs b/util/Seeder/IQuery.cs index adbcd8e59d..c21b6ebd1d 100644 --- a/util/Seeder/IQuery.cs +++ b/util/Seeder/IQuery.cs @@ -4,8 +4,8 @@ /// Base interface for query operations in the seeding system. The base interface should not be used directly, rather use `IQuery<TRequest, TResult>`. /// /// -/// Queries are synchronous, read-only operations that retrieve data from the seeding context. -/// Unlike scenes which create data, queries fetch existing data based on request parameters. +/// Queries are read-only operations that retrieve data from the seeding context. +/// Unlike scenes, which create data, queries fetch existing data based on request parameters. /// They follow a type-safe pattern using generics to ensure proper request/response handling /// while maintaining a common non-generic interface for dynamic invocation. /// @@ -22,17 +22,17 @@ public interface IQuery /// /// The request object containing parameters for the query operation. /// The query result data as an object. - object Execute(object request); + Task Execute(object request); } /// -/// Generic query interface for synchronous, read-only operations with specific request and result types. +/// Generic query interface for read-only operations with specific request and result types. /// /// The type of request object this query accepts. /// The type of data this query returns. /// /// Use this interface when you need to retrieve existing data from the seeding context based on -/// specific request parameters. Queries are synchronous and do not modify data - they only read +/// specific request parameters. Queries do not modify data - they only read /// and return information. The explicit interface implementations allow dynamic invocation while /// maintaining type safety in the implementation. /// @@ -43,7 +43,7 @@ public interface IQuery : IQuery where TRequest : class where /// /// The request object containing parameters for the query operation. /// The typed query result data. - TResult Execute(TRequest request); + Task Execute(TRequest request); /// /// Gets the request type for this query. @@ -56,5 +56,5 @@ public interface IQuery : IQuery where TRequest : class where /// /// The request object to cast and process. /// The typed result cast to object. - object IQuery.Execute(object request) => Execute((TRequest)request); + async Task IQuery.Execute(object request) => await Execute((TRequest)request); } diff --git a/util/Seeder/Queries/EmergencyAccessInviteQuery.cs b/util/Seeder/Queries/EmergencyAccessInviteQuery.cs index 95d96a9a50..d43311b310 100644 --- a/util/Seeder/Queries/EmergencyAccessInviteQuery.cs +++ b/util/Seeder/Queries/EmergencyAccessInviteQuery.cs @@ -19,7 +19,7 @@ public class EmergencyAccessInviteQuery( public required string Email { get; set; } } - public IEnumerable Execute(Request request) + public Task> Execute(Request request) { var invites = db.EmergencyAccesses .Where(ea => ea.Email == request.Email).ToList().Select(ea => @@ -30,6 +30,6 @@ public class EmergencyAccessInviteQuery( return $"/accept-emergency?id={ea.Id}&name=Dummy&email={ea.Email}&token={token}"; }); - return invites; + return Task.FromResult(invites); } } diff --git a/util/Seeder/Queries/UserEmailVerificationQuery.cs b/util/Seeder/Queries/UserEmailVerificationQuery.cs new file mode 100644 index 0000000000..ae4ab287ca --- /dev/null +++ b/util/Seeder/Queries/UserEmailVerificationQuery.cs @@ -0,0 +1,54 @@ +using System.Globalization; +using System.Net; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Repositories; +using Bit.Core.Tokens; + +namespace Bit.Seeder.Queries; + +public class UserEmailVerificationQuery(IUserRepository userRepository, + IDataProtectorTokenFactory dataProtectorTokenizer) : IQuery +{ + public class Request + { + public string? Name { get; set; } = null; + public required string Email { get; set; } + public string? FromMarketing { get; set; } = null; + public bool ReceiveMarketingEmails { get; set; } = false; + } + + public class Response + { + public required string Url { get; set; } + public required bool EmailVerified { get; set; } + } + + public async Task Execute(Request request) + { + var user = await userRepository.GetByEmailAsync(request.Email); + + var token = generateToken(request.Email, request.Name, request.ReceiveMarketingEmails); + + return new() + { + Url = Url(token, request.Email, request.FromMarketing), + EmailVerified = user?.EmailVerified ?? false + }; + } + + private string Url(string token, string email, string? fromMarketing = null) + { + return string.Format(CultureInfo.InvariantCulture, "/redirect-connector.html#finish-signup?token={0}&email={1}&fromEmail=true{2}", + WebUtility.UrlEncode(token), + WebUtility.UrlEncode(email), + !string.IsNullOrEmpty(fromMarketing) ? $"&fromMarketing={fromMarketing}" : string.Empty); + } + + private string generateToken(string email, string? name, bool receiveMarketingEmails) + { + + return dataProtectorTokenizer.Protect( + new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails) + ); + } +} diff --git a/util/SeederApi/Controllers/QueryController.cs b/util/SeederApi/Controllers/QueryController.cs index 22bf84e5b7..676c967384 100644 --- a/util/SeederApi/Controllers/QueryController.cs +++ b/util/SeederApi/Controllers/QueryController.cs @@ -9,13 +9,13 @@ namespace Bit.SeederApi.Controllers; public class QueryController(ILogger logger, IQueryExecutor queryExecutor) : Controller { [HttpPost] - public IActionResult Query([FromBody] QueryRequestModel request) + public async Task Query([FromBody] QueryRequestModel request) { logger.LogInformation("Executing query: {Query}", request.Template); try { - var result = queryExecutor.Execute(request.Template, request.Arguments); + var result = await queryExecutor.Execute(request.Template, request.Arguments); return Json(result); } diff --git a/util/SeederApi/Execution/IQueryExecutor.cs b/util/SeederApi/Execution/IQueryExecutor.cs index ebd971bbb7..53343433c6 100644 --- a/util/SeederApi/Execution/IQueryExecutor.cs +++ b/util/SeederApi/Execution/IQueryExecutor.cs @@ -18,5 +18,5 @@ public interface IQueryExecutor /// The result of the query execution /// Thrown when the query is not found /// Thrown when there's an error executing the query - object Execute(string queryName, JsonElement? arguments); + Task Execute(string queryName, JsonElement? arguments); } diff --git a/util/SeederApi/Execution/QueryExecutor.cs b/util/SeederApi/Execution/QueryExecutor.cs index 5473586c22..5e344aa23d 100644 --- a/util/SeederApi/Execution/QueryExecutor.cs +++ b/util/SeederApi/Execution/QueryExecutor.cs @@ -9,7 +9,7 @@ public class QueryExecutor( IServiceProvider serviceProvider) : IQueryExecutor { - public object Execute(string queryName, JsonElement? arguments) + public async Task Execute(string queryName, JsonElement? arguments) { try { @@ -18,7 +18,7 @@ public class QueryExecutor( var requestType = query.GetRequestType(); var requestModel = DeserializeRequestModel(queryName, requestType, arguments); - var result = query.Execute(requestModel); + var result = await query.Execute(requestModel); logger.LogInformation("Successfully executed query: {QueryName}", queryName); return result;