-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
103 lines (80 loc) · 3.4 KB
/
Copy pathProgram.cs
File metadata and controls
103 lines (80 loc) · 3.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
using PlanApi;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Scalar.AspNetCore;
using StackExchange.Redis;
using System.Text.Json.Nodes;
using Json.Schema;
using System.Linq;
using System.Text.Json;
// Top-level statements: this file IS the program (no Main method needed).
var builder = WebApplication.CreateBuilder(args);
// builder.Services is the DI container. Frozen after Build().
builder.Services.AddOpenApi();
builder.Services.AddSingleton<IConnectionMultiplexer>(_ =>
ConnectionMultiplexer.Connect(
builder.Configuration.GetConnectionString("Redis")
?? throw new InvalidOperationException("ConnectionStrings:Redis is not configured")));
builder.Services.AddSingleton(JsonSchema.FromFile(
Path.Combine(builder.Environment.ContentRootPath, "schema.json")));
builder.Services.AddSingleton<IPlanRepository, RedisRepository>();
// Resource-server auth: validate Google-issued ID tokens, never mint them.
// Authority triggers OIDC discovery + JWKS fetch, so signing keys auto-rotate.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://accounts.google.com";
options.Audience = builder.Configuration["Google:ClientId"]
?? throw new InvalidOperationException("Google:ClientId is not configured");
});
builder.Services.AddAuthorization();
var app = builder.Build();
_ = app.Services.GetRequiredService<IConnectionMultiplexer>();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.UseAuthentication();
app.UseAuthorization();
// All plan endpoints require a valid Bearer token; unauthenticated -> 401.
var plan = app.MapGroup("/v1/plan").RequireAuthorization();
plan.MapPost("", async (JsonNode body, IPlanRepository repo, JsonSchema schema, HttpResponse response) =>
{
var result = schema.Evaluate(body.Deserialize<JsonElement>(), new EvaluationOptions { OutputFormat = OutputFormat.List });
if (!result.IsValid)
{
var errors = result.Details
.Where(d => d.Errors is not null && d.Errors.Count > 0)
.SelectMany(d => d.Errors!.Select(e => new
{
path = d.InstanceLocation.ToString(),
keyword = e.Key,
message = e.Value
}))
.ToList();
return Results.BadRequest(new { errors });
}
var objectId = body["objectId"]!.GetValue<string>();
if (await repo.ExistsAsync(objectId))
return Results.Conflict(new { error = $"plan '{objectId}' already exists" });
await repo.SaveFlattenedAsync(PlanFlattener.Decompose(body));
response.Headers.ETag = ETag.Compute(body);
return Results.Created($"/v1/plan/{objectId}", null);
});
plan.MapGet("/{objectId}", async (string objectId, IPlanRepository repo, HttpRequest request, HttpResponse response) =>
{
var plan = await repo.GetAsync(objectId);
if (plan is null) return Results.NotFound();
var etag = ETag.Compute(plan);
response.Headers.ETag = etag;
if (request.Headers.IfNoneMatch.Contains(etag))
return Results.StatusCode(StatusCodes.Status304NotModified);
return Results.Ok(plan);
});
plan.MapDelete("/{objectId}", async (string objectId, IPlanRepository repo) =>
{
var deleted = await repo.DeleteAsync(objectId);
return deleted ? Results.NoContent() : Results.NotFound();
});
app.UseHttpsRedirection();
app.Run();