diff --git a/util/SeederApi/Controllers/SeedController.cs b/util/SeederApi/Controllers/SeedController.cs index 1022db8a7f..9c4282f07c 100644 --- a/util/SeederApi/Controllers/SeedController.cs +++ b/util/SeederApi/Controllers/SeedController.cs @@ -1,7 +1,6 @@ -using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.SeederApi.Services; using Microsoft.AspNetCore.Mvc; using System.ComponentModel.DataAnnotations; -using System.Reflection; using System.Text.Json; namespace Bit.SeederApi.Controllers; @@ -17,12 +16,12 @@ public class SeedRequestModel public class SeedController : Controller { private readonly ILogger _logger; - private readonly DatabaseContext _databaseContext; + private readonly IRecipeService _recipeService; - public SeedController(ILogger logger, DatabaseContext databaseContext) + public SeedController(ILogger logger, IRecipeService recipeService) { _logger = logger; - _databaseContext = databaseContext; + _recipeService = recipeService; } [HttpPost("/seed")] @@ -32,87 +31,35 @@ public class SeedController : Controller try { - // Find the recipe class - var recipeTypeName = $"Bit.Seeder.Recipes.{request.Template}"; - var recipeType = Assembly.Load("Seeder") - .GetTypes() - .FirstOrDefault(t => t.FullName == recipeTypeName); - - if (recipeType == null) - { - return NotFound(new { Error = $"Recipe '{request.Template}' not found" }); - } - - // Instantiate the recipe with DatabaseContext - var recipeInstance = Activator.CreateInstance(recipeType, _databaseContext); - if (recipeInstance == null) - { - return StatusCode(500, new { Error = "Failed to instantiate recipe" }); - } - - // Find the Seed method - var seedMethod = recipeType.GetMethod("Seed"); - if (seedMethod == null) - { - return StatusCode(500, new { Error = $"Seed method not found in recipe '{request.Template}'" }); - } - - // Parse arguments and match to method parameters - var parameters = seedMethod.GetParameters(); - var arguments = new object?[parameters.Length]; - - if (request.Arguments == null && parameters.Length > 0) - { - return BadRequest(new { Error = "Arguments are required for this recipe" }); - } - - for (int i = 0; i < parameters.Length; i++) - { - var parameter = parameters[i]; - var parameterName = parameter.Name!; - - if (request.Arguments?.TryGetProperty(parameterName, out JsonElement value) == true) - { - try - { - arguments[i] = JsonSerializer.Deserialize(value.GetRawText(), parameter.ParameterType); - } - catch (JsonException ex) - { - return BadRequest(new - { - Error = $"Failed to deserialize parameter '{parameterName}'", - Details = ex.Message - }); - } - } - else if (!parameter.HasDefaultValue) - { - return BadRequest(new { Error = $"Missing required parameter: {parameterName}" }); - } - else - { - arguments[i] = parameter.DefaultValue; - } - } - - // Invoke the Seed method - var result = seedMethod.Invoke(recipeInstance, arguments); + var result = _recipeService.ExecuteRecipe(request.Template, request.Arguments); return Ok(new { Message = "Seed completed successfully", - Template = request.Template, + request.Template, Result = result }); } + catch (RecipeNotFoundException ex) + { + return NotFound(new { Error = ex.Message }); + } + catch (RecipeExecutionException ex) + { + _logger.LogError(ex, "Error executing recipe: {Template}", request.Template); + return BadRequest(new + { + Error = ex.Message, + Details = ex.InnerException?.Message + }); + } catch (Exception ex) { - _logger.LogError(ex, "Error seeding with template: {Template}", request.Template); + _logger.LogError(ex, "Unexpected error seeding with template: {Template}", request.Template); return StatusCode(500, new { - Error = "An error occurred while seeding", - Details = ex.InnerException?.Message ?? ex.Message + Error = "An unexpected error occurred while seeding", + Details = ex.Message }); } } diff --git a/util/SeederApi/Program.cs b/util/SeederApi/Program.cs index ccaedd31a8..e1356afe46 100644 --- a/util/SeederApi/Program.cs +++ b/util/SeederApi/Program.cs @@ -1,3 +1,4 @@ +using Bit.SeederApi.Services; using Bit.SharedWeb.Utilities; var builder = WebApplication.CreateBuilder(args); @@ -14,6 +15,9 @@ builder.Services.AddCustomDataProtectionServices(builder.Environment, globalSett // Repositories builder.Services.AddDatabaseRepositories(globalSettings); +// Recipe Service +builder.Services.AddScoped(); + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/util/SeederApi/Services/IRecipeService.cs b/util/SeederApi/Services/IRecipeService.cs new file mode 100644 index 0000000000..61e8bd1da9 --- /dev/null +++ b/util/SeederApi/Services/IRecipeService.cs @@ -0,0 +1,16 @@ +using System.Text.Json; + +namespace Bit.SeederApi.Services; + +public interface IRecipeService +{ + /// + /// Executes a recipe with the given template name and arguments. + /// + /// The name of the recipe template (e.g., "OrganizationWithUsersRecipe") + /// Optional JSON arguments to pass to the recipe's Seed method + /// The result returned by the recipe's Seed method + /// Thrown when the recipe template is not found + /// Thrown when there's an error executing the recipe + object? ExecuteRecipe(string templateName, JsonElement? arguments); +} diff --git a/util/SeederApi/Services/RecipeExceptions.cs b/util/SeederApi/Services/RecipeExceptions.cs new file mode 100644 index 0000000000..be1d3f76fb --- /dev/null +++ b/util/SeederApi/Services/RecipeExceptions.cs @@ -0,0 +1,13 @@ +namespace Bit.SeederApi.Services; + +public class RecipeNotFoundException : Exception +{ + public RecipeNotFoundException(string message) : base(message) { } +} + +public class RecipeExecutionException : Exception +{ + public RecipeExecutionException(string message) : base(message) { } + public RecipeExecutionException(string message, Exception innerException) + : base(message, innerException) { } +} diff --git a/util/SeederApi/Services/RecipeService.cs b/util/SeederApi/Services/RecipeService.cs new file mode 100644 index 0000000000..7f5f7a2e15 --- /dev/null +++ b/util/SeederApi/Services/RecipeService.cs @@ -0,0 +1,105 @@ +using Bit.Infrastructure.EntityFramework.Repositories; +using System.Reflection; +using System.Text.Json; + +namespace Bit.SeederApi.Services; + +public class RecipeService : IRecipeService +{ + private readonly DatabaseContext _databaseContext; + private readonly ILogger _logger; + + public RecipeService(DatabaseContext databaseContext, ILogger logger) + { + _databaseContext = databaseContext; + _logger = logger; + } + + public object? ExecuteRecipe(string templateName, JsonElement? arguments) + { + try + { + // Find the recipe class + var recipeTypeName = $"Bit.Seeder.Recipes.{templateName}"; + var recipeType = Assembly.Load("Seeder") + .GetTypes() + .FirstOrDefault(t => t.FullName == recipeTypeName); + + if (recipeType == null) + { + throw new RecipeNotFoundException($"Recipe '{templateName}' not found"); + } + + // Instantiate the recipe with DatabaseContext + var recipeInstance = Activator.CreateInstance(recipeType, _databaseContext); + if (recipeInstance == null) + { + throw new RecipeExecutionException("Failed to instantiate recipe"); + } + + // Find the Seed method + var seedMethod = recipeType.GetMethod("Seed"); + if (seedMethod == null) + { + throw new RecipeExecutionException($"Seed method not found in recipe '{templateName}'"); + } + + // Parse arguments and match to method parameters + var parameters = seedMethod.GetParameters(); + var methodArguments = new object?[parameters.Length]; + + if (arguments == null && parameters.Length > 0) + { + throw new RecipeExecutionException("Arguments are required for this recipe"); + } + + for (var i = 0; i < parameters.Length; i++) + { + var parameter = parameters[i]; + var parameterName = parameter.Name!; + + if (arguments?.TryGetProperty(parameterName, out JsonElement value) == true) + { + try + { + methodArguments[i] = JsonSerializer.Deserialize(value.GetRawText(), parameter.ParameterType); + } + catch (JsonException ex) + { + throw new RecipeExecutionException( + $"Failed to deserialize parameter '{parameterName}': {ex.Message}", ex); + } + } + else if (!parameter.HasDefaultValue) + { + throw new RecipeExecutionException($"Missing required parameter: {parameterName}"); + } + else + { + methodArguments[i] = parameter.DefaultValue; + } + } + + // Invoke the Seed method + var result = seedMethod.Invoke(recipeInstance, methodArguments); + _logger.LogInformation("Successfully executed recipe: {TemplateName}", templateName); + + return result; + } + catch (RecipeNotFoundException) + { + throw; + } + catch (RecipeExecutionException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error executing recipe: {TemplateName}", templateName); + throw new RecipeExecutionException( + $"An unexpected error occurred while executing recipe '{templateName}'", + ex.InnerException ?? ex); + } + } +}