old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
|
||||
// Sprint: SPRINT_20260112_004_PLATFORM_setup_wizard_backend (PLATFORM-SETUP-003)
|
||||
// Task: Add /api/v1/setup/* endpoints with auth policies, request validation, and Problem+JSON errors
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Setup wizard API endpoints aligned to docs/setup/setup-wizard-ux.md.
|
||||
/// </summary>
|
||||
public static class SetupEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapSetupEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var setup = app.MapGroup("/api/v1/setup")
|
||||
.WithTags("Setup Wizard");
|
||||
|
||||
MapSessionEndpoints(setup);
|
||||
MapStepEndpoints(setup);
|
||||
MapDefinitionEndpoints(setup);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static void MapSessionEndpoints(IEndpointRouteBuilder setup)
|
||||
{
|
||||
var sessions = setup.MapGroup("/sessions").WithTags("Setup Sessions");
|
||||
|
||||
// GET /api/v1/setup/sessions - Get current session
|
||||
sessions.MapGet("/", async Task<IResult> (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
PlatformSetupService service,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await service.GetSessionAsync(requestContext!, ct).ConfigureAwait(false);
|
||||
if (result is null)
|
||||
{
|
||||
return Results.NotFound(CreateProblem(
|
||||
"Session Not Found",
|
||||
"No active setup session for this tenant.",
|
||||
StatusCodes.Status404NotFound));
|
||||
}
|
||||
return Results.Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(CreateProblem("Invalid Operation", ex.Message, StatusCodes.Status400BadRequest));
|
||||
}
|
||||
}).RequireAuthorization(PlatformPolicies.SetupRead)
|
||||
.WithName("GetSetupSession")
|
||||
.Produces<SetupSessionResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status404NotFound);
|
||||
|
||||
// POST /api/v1/setup/sessions - Create new session
|
||||
sessions.MapPost("/", async Task<IResult> (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
PlatformSetupService service,
|
||||
[FromBody] CreateSetupSessionRequest? request,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await service.CreateSessionAsync(
|
||||
requestContext!,
|
||||
request ?? new CreateSetupSessionRequest(),
|
||||
ct).ConfigureAwait(false);
|
||||
return Results.Created($"/api/v1/setup/sessions", result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(CreateProblem("Invalid Operation", ex.Message, StatusCodes.Status400BadRequest));
|
||||
}
|
||||
}).RequireAuthorization(PlatformPolicies.SetupWrite)
|
||||
.WithName("CreateSetupSession")
|
||||
.Produces<SetupSessionResponse>(StatusCodes.Status201Created)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest);
|
||||
|
||||
// POST /api/v1/setup/sessions/resume - Resume or create session
|
||||
sessions.MapPost("/resume", async Task<IResult> (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
PlatformSetupService service,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await service.ResumeOrCreateSessionAsync(requestContext!, ct).ConfigureAwait(false);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(CreateProblem("Invalid Operation", ex.Message, StatusCodes.Status400BadRequest));
|
||||
}
|
||||
}).RequireAuthorization(PlatformPolicies.SetupWrite)
|
||||
.WithName("ResumeSetupSession")
|
||||
.Produces<SetupSessionResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest);
|
||||
|
||||
// POST /api/v1/setup/sessions/finalize - Finalize session
|
||||
sessions.MapPost("/finalize", async Task<IResult> (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
PlatformSetupService service,
|
||||
[FromBody] FinalizeSetupSessionRequest? request,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await service.FinalizeSessionAsync(
|
||||
requestContext!,
|
||||
request ?? new FinalizeSetupSessionRequest(),
|
||||
ct).ConfigureAwait(false);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(CreateProblem("Invalid Operation", ex.Message, StatusCodes.Status400BadRequest));
|
||||
}
|
||||
}).RequireAuthorization(PlatformPolicies.SetupWrite)
|
||||
.WithName("FinalizeSetupSession")
|
||||
.Produces<FinalizeSetupSessionResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
private static void MapStepEndpoints(IEndpointRouteBuilder setup)
|
||||
{
|
||||
var steps = setup.MapGroup("/steps").WithTags("Setup Steps");
|
||||
|
||||
// POST /api/v1/setup/steps/execute - Execute a step
|
||||
steps.MapPost("/execute", async Task<IResult> (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
PlatformSetupService service,
|
||||
[FromBody] ExecuteSetupStepRequest request,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
if (request is null)
|
||||
{
|
||||
return Results.BadRequest(CreateProblem(
|
||||
"Invalid Request",
|
||||
"Request body is required with stepId.",
|
||||
StatusCodes.Status400BadRequest));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await service.ExecuteStepAsync(requestContext!, request, ct).ConfigureAwait(false);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(CreateProblem("Invalid Operation", ex.Message, StatusCodes.Status400BadRequest));
|
||||
}
|
||||
}).RequireAuthorization(PlatformPolicies.SetupWrite)
|
||||
.WithName("ExecuteSetupStep")
|
||||
.Produces<ExecuteSetupStepResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest);
|
||||
|
||||
// POST /api/v1/setup/steps/skip - Skip a step
|
||||
steps.MapPost("/skip", async Task<IResult> (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
PlatformSetupService service,
|
||||
[FromBody] SkipSetupStepRequest request,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
if (request is null)
|
||||
{
|
||||
return Results.BadRequest(CreateProblem(
|
||||
"Invalid Request",
|
||||
"Request body is required with stepId.",
|
||||
StatusCodes.Status400BadRequest));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await service.SkipStepAsync(requestContext!, request, ct).ConfigureAwait(false);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(CreateProblem("Invalid Operation", ex.Message, StatusCodes.Status400BadRequest));
|
||||
}
|
||||
}).RequireAuthorization(PlatformPolicies.SetupWrite)
|
||||
.WithName("SkipSetupStep")
|
||||
.Produces<SetupSessionResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
private static void MapDefinitionEndpoints(IEndpointRouteBuilder setup)
|
||||
{
|
||||
var definitions = setup.MapGroup("/definitions").WithTags("Setup Definitions");
|
||||
|
||||
// GET /api/v1/setup/definitions/steps - Get all step definitions
|
||||
definitions.MapGet("/steps", async Task<IResult> (
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
PlatformSetupService service,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
||||
{
|
||||
return failure!;
|
||||
}
|
||||
|
||||
var result = await service.GetStepDefinitionsAsync(ct).ConfigureAwait(false);
|
||||
return Results.Ok(result);
|
||||
}).RequireAuthorization(PlatformPolicies.SetupRead)
|
||||
.WithName("GetSetupStepDefinitions")
|
||||
.Produces<SetupStepDefinitionsResponse>(StatusCodes.Status200OK);
|
||||
}
|
||||
|
||||
private static bool TryResolveContext(
|
||||
HttpContext context,
|
||||
PlatformRequestContextResolver resolver,
|
||||
out PlatformRequestContext? requestContext,
|
||||
out IResult? failure)
|
||||
{
|
||||
if (resolver.TryResolve(context, out requestContext, out var error))
|
||||
{
|
||||
failure = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
failure = Results.BadRequest(CreateProblem(
|
||||
"Context Resolution Failed",
|
||||
error ?? "Unable to resolve tenant context.",
|
||||
StatusCodes.Status400BadRequest));
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ProblemDetails CreateProblem(string title, string detail, int statusCode)
|
||||
{
|
||||
return new ProblemDetails
|
||||
{
|
||||
Title = title,
|
||||
Detail = detail,
|
||||
Status = statusCode,
|
||||
Type = $"https://stella.ops/problems/{title.ToLowerInvariant().Replace(' ', '-')}"
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user