2026-04-21 20:08:49 +01:00
|
|
|
using System.Net.Http.Headers;
|
|
|
|
|
using System.Security.Claims;
|
|
|
|
|
using System.Text.Json;
|
2026-06-09 20:27:46 +01:00
|
|
|
using System.Text.Json.Serialization;
|
2026-04-21 20:08:49 +01:00
|
|
|
using Bifrost.Web.Configuration;
|
|
|
|
|
using Bifrost.Web.Data;
|
2026-04-22 09:50:31 +01:00
|
|
|
using Bifrost.Web.Features.GitHub;
|
2026-06-08 21:04:51 +01:00
|
|
|
using Bifrost.Web.Features.Heimdall;
|
2026-04-21 20:08:49 +01:00
|
|
|
using Bifrost.Web.Features.Membership;
|
2026-06-09 20:27:46 +01:00
|
|
|
using Bifrost.Web.Features.Motions;
|
2026-06-09 16:28:30 +01:00
|
|
|
using Bifrost.Web.Features.Patronage;
|
2026-04-21 20:08:49 +01:00
|
|
|
using Bifrost.Web.Features.Shared;
|
|
|
|
|
using Microsoft.AspNetCore.Authentication;
|
|
|
|
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
|
|
|
using Microsoft.AspNetCore.Authentication.OAuth;
|
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
2026-05-20 19:07:45 +01:00
|
|
|
using Microsoft.AspNetCore.DataProtection;
|
2026-04-22 09:50:31 +01:00
|
|
|
using Microsoft.AspNetCore.HttpLogging;
|
2026-04-21 20:08:49 +01:00
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
|
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
|
|
|
|
|
|
builder.Services
|
|
|
|
|
.AddOptions<GitHubOAuthOptions>()
|
|
|
|
|
.BindConfiguration(GitHubOAuthOptions.SectionName);
|
|
|
|
|
|
2026-04-22 09:50:31 +01:00
|
|
|
builder.Services
|
|
|
|
|
.AddOptions<GitHubAppOptions>()
|
|
|
|
|
.BindConfiguration(GitHubAppOptions.SectionName);
|
|
|
|
|
|
2026-04-21 20:08:49 +01:00
|
|
|
builder.Services
|
|
|
|
|
.AddOptions<BootstrapOptions>()
|
|
|
|
|
.BindConfiguration(BootstrapOptions.SectionName);
|
|
|
|
|
|
2026-04-22 09:50:31 +01:00
|
|
|
builder.Services
|
|
|
|
|
.AddOptions<BifrostHostOptions>()
|
|
|
|
|
.BindConfiguration(BifrostHostOptions.SectionName);
|
|
|
|
|
|
2026-06-08 21:04:51 +01:00
|
|
|
builder.Services
|
|
|
|
|
.AddOptions<HeimdallOptions>()
|
|
|
|
|
.BindConfiguration(HeimdallOptions.SectionName);
|
|
|
|
|
|
2026-04-21 20:08:49 +01:00
|
|
|
builder.Services.AddRazorPages(options =>
|
|
|
|
|
{
|
|
|
|
|
options.Conventions.AuthorizePage("/Membership/Status");
|
|
|
|
|
options.Conventions.AuthorizeFolder("/App", ActiveMemberRequirement.PolicyName);
|
|
|
|
|
});
|
2026-06-09 20:27:46 +01:00
|
|
|
builder.Services.ConfigureHttpJsonOptions(options =>
|
|
|
|
|
{
|
|
|
|
|
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
|
|
|
|
});
|
2026-04-21 20:08:49 +01:00
|
|
|
|
|
|
|
|
builder.Services.AddDbContext<BifrostDbContext>(options =>
|
|
|
|
|
{
|
|
|
|
|
var connectionString = builder.Configuration.GetConnectionString("Bifrost")
|
|
|
|
|
?? "Host=127.0.0.1;Database=bifrost;Username=postgres;Password=postgres";
|
|
|
|
|
|
|
|
|
|
options.UseNpgsql(connectionString);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-22 09:50:31 +01:00
|
|
|
builder.Services.AddHttpLogging(options =>
|
|
|
|
|
{
|
|
|
|
|
options.LoggingFields =
|
|
|
|
|
HttpLoggingFields.RequestMethod |
|
|
|
|
|
HttpLoggingFields.RequestPath |
|
|
|
|
|
HttpLoggingFields.ResponseStatusCode |
|
|
|
|
|
HttpLoggingFields.Duration;
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-21 20:08:49 +01:00
|
|
|
builder.Services.AddHttpContextAccessor();
|
|
|
|
|
builder.Services.AddSingleton(TimeProvider.System);
|
|
|
|
|
builder.Services.AddScoped<ICurrentBifrostActorAccessor, CurrentBifrostActorAccessor>();
|
|
|
|
|
builder.Services.AddScoped<AuditTrailService>();
|
|
|
|
|
builder.Services.AddScoped<DashboardSnapshotService>();
|
|
|
|
|
builder.Services.AddScoped<MembershipSynchronizationService>();
|
2026-04-22 09:50:31 +01:00
|
|
|
builder.Services.AddScoped<GitHubWebhookService>();
|
2026-06-09 20:27:46 +01:00
|
|
|
builder.Services.AddScoped<MotionGovernanceService>();
|
|
|
|
|
builder.Services.AddScoped<MotionEveSurfaceService>();
|
2026-06-09 16:28:30 +01:00
|
|
|
builder.Services.AddScoped<PatronageService>();
|
2026-06-09 17:16:27 +01:00
|
|
|
builder.Services.AddScoped<HeimdallPatronSupportIntakeService>();
|
2026-04-22 09:50:31 +01:00
|
|
|
builder.Services.AddScoped<ReadinessService>();
|
|
|
|
|
builder.Services.AddScoped<ApplicationBootstrapper>();
|
|
|
|
|
builder.Services.AddSingleton<StartupConfigurationValidator>();
|
2026-04-21 20:08:49 +01:00
|
|
|
builder.Services.AddScoped<IAuthorizationHandler, ActiveMemberAuthorizationHandler>();
|
2026-06-08 21:04:51 +01:00
|
|
|
builder.Services.AddSingleton<HeimdallAuthAttemptStore>();
|
|
|
|
|
builder.Services.AddHttpClient<HeimdallAuthService>((serviceProvider, client) =>
|
|
|
|
|
{
|
|
|
|
|
var options = serviceProvider.GetRequiredService<IOptions<HeimdallOptions>>().Value;
|
|
|
|
|
client.BaseAddress = new Uri(options.BaseUrl.TrimEnd('/'));
|
|
|
|
|
});
|
2026-04-21 20:08:49 +01:00
|
|
|
|
2026-05-20 19:07:45 +01:00
|
|
|
var dataProtectionKeysPath = builder.Configuration["DataProtection:KeysPath"];
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(dataProtectionKeysPath))
|
|
|
|
|
{
|
|
|
|
|
builder.Services
|
|
|
|
|
.AddDataProtection()
|
|
|
|
|
.PersistKeysToFileSystem(new DirectoryInfo(dataProtectionKeysPath));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var configuredGitHubOAuthOptions = builder.Configuration
|
|
|
|
|
.GetSection(GitHubOAuthOptions.SectionName)
|
|
|
|
|
.Get<GitHubOAuthOptions>() ?? new GitHubOAuthOptions();
|
|
|
|
|
|
|
|
|
|
var authenticationBuilder = builder.Services
|
2026-04-21 20:08:49 +01:00
|
|
|
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
|
|
|
|
.AddCookie(options =>
|
|
|
|
|
{
|
|
|
|
|
options.LoginPath = "/auth/sign-in";
|
|
|
|
|
options.AccessDeniedPath = "/Membership/Status";
|
|
|
|
|
options.SlidingExpiration = true;
|
2026-05-20 19:07:45 +01:00
|
|
|
});
|
2026-04-21 20:08:49 +01:00
|
|
|
|
2026-05-20 19:07:45 +01:00
|
|
|
if (configuredGitHubOAuthOptions.IsConfigured)
|
|
|
|
|
{
|
|
|
|
|
authenticationBuilder.AddOAuth(GitHubAuthenticationDefaults.AuthenticationScheme, options =>
|
|
|
|
|
{
|
|
|
|
|
options.ClientId = configuredGitHubOAuthOptions.ClientId;
|
|
|
|
|
options.ClientSecret = configuredGitHubOAuthOptions.ClientSecret;
|
|
|
|
|
options.CallbackPath = configuredGitHubOAuthOptions.CallbackPath;
|
2026-04-21 20:08:49 +01:00
|
|
|
options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
|
|
|
|
|
options.TokenEndpoint = "https://github.com/login/oauth/access_token";
|
|
|
|
|
options.UserInformationEndpoint = "https://api.github.com/user";
|
|
|
|
|
options.SaveTokens = true;
|
|
|
|
|
options.Scope.Add("read:user");
|
|
|
|
|
options.Scope.Add("user:email");
|
|
|
|
|
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
|
|
|
|
|
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
|
|
|
|
|
options.ClaimActions.MapJsonKey(BifrostClaimTypes.GitHubLogin, "login");
|
|
|
|
|
options.ClaimActions.MapJsonKey(BifrostClaimTypes.DisplayName, "name");
|
|
|
|
|
options.ClaimActions.MapJsonKey(BifrostClaimTypes.AvatarUrl, "avatar_url");
|
|
|
|
|
|
|
|
|
|
options.Events = new OAuthEvents
|
|
|
|
|
{
|
|
|
|
|
OnCreatingTicket = async context =>
|
|
|
|
|
{
|
|
|
|
|
using var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
|
|
|
|
|
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
|
|
|
|
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
|
|
|
|
|
request.Headers.UserAgent.ParseAdd("Bifrost");
|
|
|
|
|
|
|
|
|
|
using var response = await context.Backchannel.SendAsync(
|
|
|
|
|
request,
|
|
|
|
|
HttpCompletionOption.ResponseHeadersRead,
|
|
|
|
|
context.HttpContext.RequestAborted);
|
|
|
|
|
|
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
|
|
|
|
|
|
await using var payloadStream = await response.Content.ReadAsStreamAsync(context.HttpContext.RequestAborted);
|
|
|
|
|
using var payload = await JsonDocument.ParseAsync(payloadStream, cancellationToken: context.HttpContext.RequestAborted);
|
|
|
|
|
context.RunClaimActions(payload.RootElement);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
});
|
2026-05-20 19:07:45 +01:00
|
|
|
}
|
2026-04-21 20:08:49 +01:00
|
|
|
|
|
|
|
|
builder.Services.AddAuthorization(options =>
|
|
|
|
|
{
|
|
|
|
|
options.AddPolicy(ActiveMemberRequirement.PolicyName, policy =>
|
|
|
|
|
{
|
|
|
|
|
policy.RequireAuthenticatedUser();
|
|
|
|
|
policy.AddRequirements(new ActiveMemberRequirement());
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var app = builder.Build();
|
2026-06-09 20:27:46 +01:00
|
|
|
var eveJsonSerializerOptions = CreateEveJsonSerializerOptions();
|
2026-04-21 20:08:49 +01:00
|
|
|
|
2026-04-22 09:50:31 +01:00
|
|
|
app.Services.GetRequiredService<StartupConfigurationValidator>().Validate();
|
|
|
|
|
await using (var scope = app.Services.CreateAsyncScope())
|
|
|
|
|
{
|
|
|
|
|
await scope.ServiceProvider
|
|
|
|
|
.GetRequiredService<ApplicationBootstrapper>()
|
|
|
|
|
.RunAsync(CancellationToken.None);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 20:08:49 +01:00
|
|
|
if (!app.Environment.IsDevelopment())
|
|
|
|
|
{
|
|
|
|
|
app.UseExceptionHandler("/Error");
|
|
|
|
|
app.UseHsts();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 09:50:31 +01:00
|
|
|
app.UseHttpLogging();
|
2026-04-21 20:08:49 +01:00
|
|
|
app.UseHttpsRedirection();
|
|
|
|
|
app.UseStaticFiles();
|
|
|
|
|
app.UseRouting();
|
|
|
|
|
app.UseAuthentication();
|
|
|
|
|
app.UseMiddleware<GitHubMembershipSynchronizationMiddleware>();
|
|
|
|
|
app.UseAuthorization();
|
|
|
|
|
|
2026-04-22 09:50:31 +01:00
|
|
|
app.MapGet("/healthz", () => Results.Text("ok")).AllowAnonymous();
|
|
|
|
|
|
|
|
|
|
app.MapGet("/readyz", async (ReadinessService readinessService, CancellationToken cancellationToken) =>
|
|
|
|
|
{
|
|
|
|
|
var report = await readinessService.GetReportAsync(cancellationToken);
|
|
|
|
|
return report.IsReady
|
|
|
|
|
? Results.Text("ready")
|
|
|
|
|
: Results.Text(
|
|
|
|
|
string.Join(Environment.NewLine, report.Failures),
|
|
|
|
|
statusCode: StatusCodes.Status503ServiceUnavailable);
|
|
|
|
|
}).AllowAnonymous();
|
|
|
|
|
|
|
|
|
|
app.MapPost("/github/webhooks", async (
|
|
|
|
|
HttpRequest request,
|
|
|
|
|
GitHubWebhookService gitHubWebhookService,
|
|
|
|
|
CancellationToken cancellationToken) =>
|
|
|
|
|
{
|
|
|
|
|
using var reader = new StreamReader(request.Body);
|
|
|
|
|
var payload = await reader.ReadToEndAsync(cancellationToken);
|
|
|
|
|
var eventName = request.Headers["X-GitHub-Event"].ToString();
|
|
|
|
|
var deliveryId = request.Headers["X-GitHub-Delivery"].ToString();
|
|
|
|
|
var signature = request.Headers["X-Hub-Signature-256"].ToString();
|
|
|
|
|
|
|
|
|
|
var result = await gitHubWebhookService.ProcessAsync(
|
|
|
|
|
eventName,
|
|
|
|
|
deliveryId,
|
|
|
|
|
signature,
|
|
|
|
|
payload,
|
|
|
|
|
cancellationToken);
|
|
|
|
|
|
|
|
|
|
return result.Status switch
|
|
|
|
|
{
|
|
|
|
|
GitHubWebhookProcessStatus.Processed => Results.Text(result.Message, statusCode: StatusCodes.Status202Accepted),
|
|
|
|
|
GitHubWebhookProcessStatus.Ignored => Results.Text(result.Message),
|
|
|
|
|
GitHubWebhookProcessStatus.NotConfigured => Results.Text(
|
|
|
|
|
result.Message,
|
|
|
|
|
statusCode: StatusCodes.Status503ServiceUnavailable),
|
|
|
|
|
_ => Results.Text(
|
|
|
|
|
result.Message,
|
|
|
|
|
statusCode: StatusCodes.Status401Unauthorized)
|
|
|
|
|
};
|
|
|
|
|
}).AllowAnonymous();
|
|
|
|
|
|
2026-04-21 20:08:49 +01:00
|
|
|
app.MapGet("/auth/sign-in", async (HttpContext httpContext, IOptions<GitHubOAuthOptions> gitHubOptions) =>
|
|
|
|
|
{
|
|
|
|
|
if (!gitHubOptions.Value.IsConfigured)
|
|
|
|
|
{
|
|
|
|
|
httpContext.Response.Redirect("/?auth=github-not-configured");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var redirectUri = httpContext.Request.Query["returnUrl"].ToString();
|
|
|
|
|
if (string.IsNullOrWhiteSpace(redirectUri))
|
|
|
|
|
{
|
|
|
|
|
redirectUri = "/App";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await httpContext.ChallengeAsync(
|
|
|
|
|
GitHubAuthenticationDefaults.AuthenticationScheme,
|
|
|
|
|
new() { RedirectUri = redirectUri });
|
|
|
|
|
}).AllowAnonymous();
|
|
|
|
|
|
2026-06-08 21:04:51 +01:00
|
|
|
app.MapGet("/auth/heimdall/{provider}", async (
|
|
|
|
|
string provider,
|
|
|
|
|
HttpContext httpContext,
|
|
|
|
|
HeimdallAuthService heimdallAuthService,
|
|
|
|
|
IOptions<BifrostHostOptions> hostOptions,
|
|
|
|
|
CancellationToken cancellationToken) =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var returnUrl = httpContext.Request.Query["returnUrl"].ToString();
|
|
|
|
|
if (string.IsNullOrWhiteSpace(returnUrl))
|
|
|
|
|
{
|
|
|
|
|
returnUrl = "/App";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var start = await heimdallAuthService.StartAsync(
|
|
|
|
|
provider,
|
|
|
|
|
hostOptions.Value.PublicBaseUrl,
|
|
|
|
|
returnUrl,
|
|
|
|
|
cancellationToken);
|
|
|
|
|
|
|
|
|
|
return Results.Redirect(start.AuthorizationUrl.ToString());
|
|
|
|
|
}
|
|
|
|
|
catch (HeimdallAuthException error)
|
|
|
|
|
{
|
|
|
|
|
return Results.Redirect($"/?auth=heimdall-error&detail={Uri.EscapeDataString(error.Message)}");
|
|
|
|
|
}
|
|
|
|
|
}).AllowAnonymous();
|
|
|
|
|
|
|
|
|
|
app.MapPost("/auth/heimdall/callback", (
|
|
|
|
|
HeimdallBackendCallbackPayload payload,
|
|
|
|
|
HeimdallAuthService heimdallAuthService) =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
heimdallAuthService.StoreBackendCallback(payload);
|
|
|
|
|
return Results.Accepted();
|
|
|
|
|
}
|
|
|
|
|
catch (HeimdallAuthException error)
|
|
|
|
|
{
|
|
|
|
|
return Results.BadRequest(error.Message);
|
|
|
|
|
}
|
|
|
|
|
}).AllowAnonymous();
|
|
|
|
|
|
2026-06-09 17:16:27 +01:00
|
|
|
app.MapPost("/heimdall/patron-support/events", async (
|
|
|
|
|
HttpRequest request,
|
|
|
|
|
HeimdallPatronSupportIntakeService intakeService,
|
|
|
|
|
CancellationToken cancellationToken) =>
|
|
|
|
|
{
|
|
|
|
|
using var reader = new StreamReader(request.Body);
|
|
|
|
|
var payload = await reader.ReadToEndAsync(cancellationToken);
|
|
|
|
|
var signature = request.Headers["X-Heimdall-Signature-256"].ToString();
|
|
|
|
|
|
|
|
|
|
var result = await intakeService.ProcessAsync(signature, payload, cancellationToken);
|
|
|
|
|
return result.Status switch
|
|
|
|
|
{
|
|
|
|
|
HeimdallPatronSupportIntakeStatus.Processed => Results.Text(
|
|
|
|
|
result.Message,
|
|
|
|
|
statusCode: StatusCodes.Status202Accepted),
|
|
|
|
|
HeimdallPatronSupportIntakeStatus.BadRequest => Results.BadRequest(result.Message),
|
|
|
|
|
HeimdallPatronSupportIntakeStatus.NotConfigured => Results.Text(
|
|
|
|
|
result.Message,
|
|
|
|
|
statusCode: StatusCodes.Status503ServiceUnavailable),
|
|
|
|
|
_ => Results.Text(result.Message, statusCode: StatusCodes.Status401Unauthorized)
|
|
|
|
|
};
|
|
|
|
|
}).AllowAnonymous();
|
|
|
|
|
|
2026-06-09 20:27:46 +01:00
|
|
|
var eveGovernance = app.MapGroup("/eve/governance")
|
|
|
|
|
.RequireAuthorization(ActiveMemberRequirement.PolicyName);
|
|
|
|
|
|
|
|
|
|
eveGovernance.MapGet("/surface", async (
|
|
|
|
|
ICurrentBifrostActorAccessor actorAccessor,
|
|
|
|
|
MotionGovernanceService motionGovernanceService,
|
|
|
|
|
MotionEveSurfaceService motionEveSurfaceService,
|
|
|
|
|
CancellationToken cancellationToken) =>
|
|
|
|
|
{
|
|
|
|
|
var actor = await actorAccessor.GetAsync(cancellationToken);
|
|
|
|
|
var state = await motionGovernanceService.GetStateAsync(actor, cancellationToken);
|
|
|
|
|
var surface = motionEveSurfaceService.BuildSurface(state);
|
|
|
|
|
return Results.Text(
|
|
|
|
|
JsonSerializer.Serialize(surface, eveJsonSerializerOptions),
|
|
|
|
|
"application/json");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
eveGovernance.MapPost("/commands", async (
|
|
|
|
|
MotionEveCommandRequest command,
|
|
|
|
|
ICurrentBifrostActorAccessor actorAccessor,
|
|
|
|
|
MotionGovernanceService motionGovernanceService,
|
|
|
|
|
MotionEveSurfaceService motionEveSurfaceService,
|
|
|
|
|
CancellationToken cancellationToken) =>
|
|
|
|
|
{
|
|
|
|
|
var actor = await actorAccessor.GetAsync(cancellationToken);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
switch (command.Command)
|
|
|
|
|
{
|
|
|
|
|
case "motion.create":
|
|
|
|
|
if (command.Scope is null ||
|
|
|
|
|
command.Category is null ||
|
|
|
|
|
command.ClosesAtUtc is null ||
|
|
|
|
|
string.IsNullOrWhiteSpace(command.Title) ||
|
|
|
|
|
string.IsNullOrWhiteSpace(command.Summary))
|
|
|
|
|
{
|
|
|
|
|
return Results.BadRequest("motion.create requires scope, category, title, summary, and closesAtUtc.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await motionGovernanceService.CreateMotionAsync(
|
|
|
|
|
actor,
|
|
|
|
|
new CreateMotionCommand(
|
|
|
|
|
command.Scope.Value,
|
|
|
|
|
command.ProjectId,
|
|
|
|
|
command.Category.Value,
|
|
|
|
|
command.Title,
|
|
|
|
|
command.Summary,
|
|
|
|
|
command.ClosesAtUtc.Value),
|
|
|
|
|
cancellationToken);
|
|
|
|
|
break;
|
|
|
|
|
case "motion.vote":
|
|
|
|
|
if (command.MotionId is null || command.Choice is null)
|
|
|
|
|
{
|
|
|
|
|
return Results.BadRequest("motion.vote requires motionId and choice.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await motionGovernanceService.CastVoteAsync(
|
|
|
|
|
actor,
|
|
|
|
|
command.MotionId.Value,
|
|
|
|
|
command.Choice.Value,
|
|
|
|
|
command.Comment,
|
|
|
|
|
cancellationToken);
|
|
|
|
|
break;
|
|
|
|
|
case "motion.close":
|
|
|
|
|
if (command.MotionId is null)
|
|
|
|
|
{
|
|
|
|
|
return Results.BadRequest("motion.close requires motionId.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await motionGovernanceService.CloseMotionAsync(actor, command.MotionId.Value, cancellationToken);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return Results.BadRequest($"Unknown Eve governance command '{command.Command}'.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (UnauthorizedAccessException)
|
|
|
|
|
{
|
|
|
|
|
return Results.Forbid();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var state = await motionGovernanceService.GetStateAsync(actor, cancellationToken);
|
|
|
|
|
var surface = motionEveSurfaceService.BuildSurface(state);
|
|
|
|
|
return Results.Text(
|
|
|
|
|
JsonSerializer.Serialize(surface, eveJsonSerializerOptions),
|
|
|
|
|
"application/json");
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-08 21:04:51 +01:00
|
|
|
app.MapGet("/auth/heimdall/wait", async (
|
|
|
|
|
HttpContext httpContext,
|
|
|
|
|
HeimdallAuthService heimdallAuthService,
|
|
|
|
|
CancellationToken cancellationToken) =>
|
|
|
|
|
{
|
|
|
|
|
var attemptId = httpContext.Request.Query["attemptId"].ToString();
|
|
|
|
|
if (string.IsNullOrWhiteSpace(attemptId))
|
|
|
|
|
{
|
|
|
|
|
return Results.Redirect("/?auth=heimdall-error&detail=missing-attempt-id");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var signIn = await heimdallAuthService.CompleteAttemptAsync(attemptId, cancellationToken);
|
|
|
|
|
await httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, signIn.Principal);
|
|
|
|
|
return Results.Redirect(signIn.RedirectUri);
|
|
|
|
|
}
|
|
|
|
|
catch (HeimdallAuthPendingException)
|
|
|
|
|
{
|
|
|
|
|
httpContext.Response.Headers.Append("Refresh", "2");
|
|
|
|
|
return Results.Text("Waiting for Heimdall sign-in to complete.", "text/plain");
|
|
|
|
|
}
|
|
|
|
|
catch (HeimdallAuthException error)
|
|
|
|
|
{
|
|
|
|
|
return Results.Redirect($"/?auth=heimdall-error&detail={Uri.EscapeDataString(error.Message)}");
|
|
|
|
|
}
|
|
|
|
|
}).AllowAnonymous();
|
|
|
|
|
|
2026-04-21 20:08:49 +01:00
|
|
|
app.MapPost("/auth/sign-out", async (HttpContext httpContext) =>
|
|
|
|
|
{
|
|
|
|
|
await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
|
|
|
|
httpContext.Response.Redirect("/");
|
|
|
|
|
}).AllowAnonymous();
|
|
|
|
|
|
|
|
|
|
app.MapRazorPages();
|
|
|
|
|
|
|
|
|
|
app.Run();
|
|
|
|
|
|
2026-06-09 20:27:46 +01:00
|
|
|
static JsonSerializerOptions CreateEveJsonSerializerOptions()
|
|
|
|
|
{
|
|
|
|
|
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
|
|
|
|
options.Converters.Add(new JsonStringEnumConverter());
|
|
|
|
|
return options;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 20:08:49 +01:00
|
|
|
public partial class Program;
|