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;