Commit Graph

18 Commits

Author SHA1 Message Date
Spencer Twaddle ad5406a5d7 Fix middleware execution order so rate limiting comes after authentication. 2026-06-20 13:43:18 -05:00
Spencer Twaddle 1c4cc3c79f Updated deploy script, and reduced logging noise 2026-05-07 06:43:04 -05:00
Spencer Twaddle ac3dcc2f31 Security & resource hardening: eliminate CPU/disk attack surface
Addresses production CPU spike incident. Key changes:

- Guard OTel exporter behind OTEL_EXPORTER_OTLP_ENDPOINT env var; filter
  tracing to /api paths only — unconditional export was primary suspect
- Remove /healthz endpoint entirely (unauthenticated, hit DB on every call)
- Replace KnownUserMiddleware with POST /api/users/me called once on login
  from TokenSync — eliminates unconditional DB write on every request
- Add DB indexes: (BudgetId, IsDeleted) on Incomes/Outgos, OwnerUserId on
  Budgets, SharedWithUserId and (IsPending, SharedWithEmail) on BudgetShares
- Move UseRateLimiter() before UseStaticFiles() so all requests are throttled
- Replace full-array reorder with move-by-position (id + newIndex) — bounded
  input, fewer DB writes, better API design
- Lock ForwardedHeaders to 172.20.0.0/16 subnet; fixes KnownNetworks
  deprecation warning (0 warnings in build now)
- Add AsNoTracking() to all read-only queries in Summary/Incomes/OutgosController
- FrequencyCalculator returns 0 for unknown enum values instead of throwing
- Thread.Sleep → await Task.Delay in OIDC startup loop
- AllowedHosts locked to budget.stwaddle.com

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 22:17:18 -05:00
Spencer Twaddle 8d4d7c7ce3 Fix OTel wiring to match working Auth implementation
Three corrections vs Auth project:
- Replace AddOtlpExporter() (traces only) with UseOtlpExporter() so both
  traces and logs are exported via OTLP
- Remove redundant .WithLogging() call; builder.Logging.AddOpenTelemetry()
  is sufficient on its own
- Pass OTEL_EXPORTER_OTLP_ENDPOINT/PROTOCOL through from host env vars
  instead of hardcoding the collector URL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 17:34:45 -05:00
Spencer Twaddle 389446ee21 Phase 1: Wire up OpenTelemetry in Budget.Api
Adds OTel packages (Extensions.Hosting, Instrumentation.AspNetCore/Http/EFCore,
Exporter.OTLP) and configures logging + tracing in Program.cs. Endpoint is
intentionally left unset in code — read from OTEL_EXPORTER_OTLP_ENDPOINT at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 17:29:01 -05:00
Spencer Twaddle 665062f0b5 Updated styling and fixed add row functionality 2026-05-03 06:15:29 -05:00
Spencer Twaddle 087fbdd176 Split into Budget.Core / Budget.Infrastructure / Budget.Api projects
Budget.Core: entities, DTOs, enums, FrequencyCalculator (no EF/ASP.NET deps)
Budget.Infrastructure: AppDbContext, migrations, BudgetAuthorizationService
Budget.Api: controllers, middleware, Program.cs — references both projects

EF and Npgsql packages moved to Infrastructure; Api retains only JwtBearer,
HealthChecks, and EF.Design (needed for dotnet ef CLI). Dockerfile updated
to copy all three project directories before publishing. Migration namespaces
updated from Budget.Api.Data.* to Budget.Infrastructure.Data.* and model
type strings updated to Budget.Core.Models.* in the snapshot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 16:30:31 -05:00
Spencer Twaddle cd42a8ec2c Add ErrorHandlingMiddleware for consistent JSON error responses
Catches all unhandled exceptions, logs them, and returns
{ "error": "An unexpected error occurred." } with HTTP 500.
Registered before all other middleware so nothing leaks through.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 15:57:13 -05:00
Spencer Twaddle 9b1b704ea1 Add rate limiting: global (120/min) and writes (30/min) policies
Both policies partition by sub claim with IP fallback. Global limiter
applies to all requests; writes policy is applied via
[EnableRateLimiting("writes")] on every POST, PUT, and DELETE action
across all five controllers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 15:56:54 -05:00
Spencer Twaddle 89e9880f76 Add ForwardedHeaders middleware for nginx-proxy
Clears KnownNetworks/KnownProxies to trust X-Forwarded-For from any
upstream, since nginx-proxy sits at a dynamically assigned internal IP.
Without this, RemoteIpAddress is always the proxy IP, breaking any
per-client IP resolution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 15:55:13 -05:00
Spencer Twaddle 3b28b89f49 Add OIDC discovery retry loop on startup
Blocks startup for up to 2 minutes retrying the OIDC discovery doc fetch,
then proceeds anyway. Prevents JWT middleware from failing to initialize
when the auth and app containers start simultaneously.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 15:54:57 -05:00
Spencer Twaddle 489f376253 Move OIDC config to appsettings.json and add MetadataAddress
Authority, Audience, and MetadataAddress are not secrets so they belong
in committed config rather than runtime env vars. MetadataAddress points
to the internal Docker URL for JWKS fetch, avoiding nginx hairpinning;
it is blanked in Development so the JWT middleware falls back to
Authority-based discovery. RequireHttpsMetadata is disabled only when
MetadataAddress is set (internal http URL).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 15:54:39 -05:00
Spencer Twaddle 71bd88ace9 Fixed some critical bugs 2026-05-02 15:50:03 -05:00
Spencer Twaddle 6d1bc2ce2c Security hardening
- Remove OwnerUserId from BudgetDto: OIDC sub of the budget owner was
  being returned to all collaborators (including View-only users)
- Remove SharedWithUserId from ShareDto: other users' internal OIDC subs
  were visible to anyone with read access to a budget
- Delete MeController: scaffolding endpoint that returned sub to the
  browser; no legitimate frontend use case
- Restrict /healthz to require authorization: prevents unauthenticated
  probing of database connectivity
- Add input validation annotations to all request DTOs: [Required],
  [MaxLength], [Range(0,0.9999)] on EffectiveTaxRate, [EmailAddress] on
  share email — [ApiController] now returns 400 instead of 500 for
  invalid input hitting DB constraints
- Replace User.FindFirst("sub")!.Value with GetUserId() extension across
  all controllers: returns 401 instead of NullReferenceException (500)
  if a token lacks a sub claim

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 09:00:33 -05:00
Spencer Twaddle 45f921bb71 Phase 8: Polish and production readiness
- Add ErrorBoundary component wrapping the whole app
- Add ToastProvider with showError/showInfo; Income and Outgo pages use it for API errors
- Add LoadingSkeleton component with shimmer animation; Income and Outgo show it while loading
- Add confirm-on-delete dialogs for income and outgo rows
- Apply EF migrations automatically on startup via MigrateAsync()
- Add /healthz health check endpoint using DbContext check
- Add Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore package

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 08:03:05 -05:00
Spencer Twaddle 963e511287 Phase 3: Budget and sharing API
- Add BudgetsController: list (owner + shared), create, get, rename, delete
- Add BudgetAuthorizationService: Owner / Edit / View / None access levels
- Add SharesController: list, add (resolves KnownUser immediately), update permission, revoke
- Register BudgetAuthorizationService as scoped service
- Add BudgetDto, ShareDto, and associated request DTOs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 07:55:07 -05:00
Spencer Twaddle ae21da6a81 Phase 2: Authentication scaffolding
- Add JWT Bearer auth to ASP.NET (authority/audience from AUTH__AUTHORITY / AUTH__AUDIENCE config)
- Add KnownUserMiddleware: upserts KnownUser and resolves pending shares on each authenticated request
- Add MeController as a guarded test endpoint (/api/me)
- Add oidc-client-ts + react-router-dom to client
- Create AuthContext/AuthProvider with login, logout, token storage
- Create AuthGuard component protecting all budget routes
- Add stub /callback page for OIDC redirect handling
- Wire up all routes in App.tsx with SPA routing structure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 07:54:21 -05:00
Spencer Twaddle d788dfea03 Phase 1: Project scaffolding and infrastructure
- Scaffold Budget.Api (ASP.NET Core Web API, net10.0) with EF Core + Npgsql
- Scaffold Budget.Client (Vite + React + TypeScript) with /api proxy to localhost:5000
- Define all entity models: Budget, Income, Outgo, KnownUser, BudgetShare
- Configure AppDbContext with EF mappings and cascade deletes
- Add InitialCreate migration
- Configure SPA static file serving + fallback in Program.cs
- Add Dockerfile (multi-stage: node + dotnet sdk + aspnet runtime)
- Add .env.example with all required environment variables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 07:37:28 -05:00