mirror of
https://github.com/bitwarden/server
synced 2025-12-15 15:53:59 +00:00
Extract logic into service
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
using Bit.SeederApi.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Reflection;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Bit.SeederApi.Controllers;
|
namespace Bit.SeederApi.Controllers;
|
||||||
@@ -17,12 +16,12 @@ public class SeedRequestModel
|
|||||||
public class SeedController : Controller
|
public class SeedController : Controller
|
||||||
{
|
{
|
||||||
private readonly ILogger<SeedController> _logger;
|
private readonly ILogger<SeedController> _logger;
|
||||||
private readonly DatabaseContext _databaseContext;
|
private readonly IRecipeService _recipeService;
|
||||||
|
|
||||||
public SeedController(ILogger<SeedController> logger, DatabaseContext databaseContext)
|
public SeedController(ILogger<SeedController> logger, IRecipeService recipeService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_databaseContext = databaseContext;
|
_recipeService = recipeService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("/seed")]
|
[HttpPost("/seed")]
|
||||||
@@ -32,87 +31,35 @@ public class SeedController : Controller
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Find the recipe class
|
var result = _recipeService.ExecuteRecipe(request.Template, request.Arguments);
|
||||||
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);
|
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
Message = "Seed completed successfully",
|
Message = "Seed completed successfully",
|
||||||
Template = request.Template,
|
request.Template,
|
||||||
Result = result
|
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)
|
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
|
return StatusCode(500, new
|
||||||
{
|
{
|
||||||
Error = "An error occurred while seeding",
|
Error = "An unexpected error occurred while seeding",
|
||||||
Details = ex.InnerException?.Message ?? ex.Message
|
Details = ex.Message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Bit.SeederApi.Services;
|
||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -14,6 +15,9 @@ builder.Services.AddCustomDataProtectionServices(builder.Environment, globalSett
|
|||||||
// Repositories
|
// Repositories
|
||||||
builder.Services.AddDatabaseRepositories(globalSettings);
|
builder.Services.AddDatabaseRepositories(globalSettings);
|
||||||
|
|
||||||
|
// Recipe Service
|
||||||
|
builder.Services.AddScoped<IRecipeService, RecipeService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|||||||
16
util/SeederApi/Services/IRecipeService.cs
Normal file
16
util/SeederApi/Services/IRecipeService.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Bit.SeederApi.Services;
|
||||||
|
|
||||||
|
public interface IRecipeService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a recipe with the given template name and arguments.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="templateName">The name of the recipe template (e.g., "OrganizationWithUsersRecipe")</param>
|
||||||
|
/// <param name="arguments">Optional JSON arguments to pass to the recipe's Seed method</param>
|
||||||
|
/// <returns>The result returned by the recipe's Seed method</returns>
|
||||||
|
/// <exception cref="RecipeNotFoundException">Thrown when the recipe template is not found</exception>
|
||||||
|
/// <exception cref="RecipeExecutionException">Thrown when there's an error executing the recipe</exception>
|
||||||
|
object? ExecuteRecipe(string templateName, JsonElement? arguments);
|
||||||
|
}
|
||||||
13
util/SeederApi/Services/RecipeExceptions.cs
Normal file
13
util/SeederApi/Services/RecipeExceptions.cs
Normal file
@@ -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) { }
|
||||||
|
}
|
||||||
105
util/SeederApi/Services/RecipeService.cs
Normal file
105
util/SeederApi/Services/RecipeService.cs
Normal file
@@ -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<RecipeService> _logger;
|
||||||
|
|
||||||
|
public RecipeService(DatabaseContext databaseContext, ILogger<RecipeService> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user