Commit Graph

42 Commits

Author SHA1 Message Date
Spencer Twaddle 4fa35dadc3 Fix reorder: normalize SortOrder after move, remove AsNoTracking from write path
The shift-in-place approach broke when SortOrder values had gaps from soft
deletes. Switch to remove/insert then assign SortOrder = index, which is
correct regardless of initial values. Also fix OutgosController Reorder
which had AsNoTracking applied from the M-3 change, silently dropping saves.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 22:24:30 -05:00
Spencer Twaddle b82b3939ce Improve deploy script reliability
Switch from explicit down/up to --pull always to avoid unnecessary network
teardown. Add exit code check on SSH command and a post-deploy status check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 22:22:10 -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 69ec754775 Updated CLAUDE.md and deployment script 2026-05-05 07:38:31 -05:00
Spencer Twaddle efde0f952b Updated plan to fix issues 2026-05-04 17:42:44 -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 bfd5880b9c Phase 2: Add OTel env vars and telemetry network to docker-compose
Wires the budget container to the OTel Collector via the shared external
telemetry network. Endpoint, protocol, and service name come from env vars
so the collector address is not baked into application code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 17:29:30 -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 78fc982adc Fixed styling of NWS columns and summary graph 2026-05-03 07:36:04 -05:00
Spencer Twaddle f686f5fafc Added sorting to Income and Outgo 2026-05-03 07:26:46 -05:00
Spencer Twaddle f3fe1ea146 Removed tax settings 2026-05-03 07:20:19 -05:00
Spencer Twaddle 89759abcca Replaced word buttons with icons 2026-05-03 07:12:25 -05:00
Spencer Twaddle 2f165487d3 Removed tax nonsense from summary and added a donut graph 2026-05-03 07:02:33 -05:00
Spencer Twaddle 665062f0b5 Updated styling and fixed add row functionality 2026-05-03 06:15:29 -05:00
Spencer Twaddle bc9f55ef91 Phase 5: Add EF migration for soft delete columns; use shadow property for xmin
- Migration adds IsDeleted/DeletedAt to Budgets, Incomes, Outgos, BudgetShares
- xmin concurrency token configured as shadow property (uint, xid type) — not added
  as a column in the migration since xmin is a PostgreSQL system column
- Budget.RowVersion property removed; concurrency tracked via shadow property only
2026-05-02 17:17:48 -05:00
Spencer Twaddle 2908397b1e Phase 1-4: Add soft delete, concurrency token, update EF config and controllers
- Add ISoftDeletable interface with IsDeleted/DeletedAt
- Implement on Budget, Income, Outgo, BudgetShare; add RowVersion (xmin) to Budget
- Configure EF global query filters and xmin concurrency token
- Replace Remove() with soft delete in all delete endpoints
- Wrap Budget.Update SaveChanges in DbUpdateConcurrencyException catch
2026-05-02 17:14:28 -05:00
Spencer Twaddle da6eb547ce Phase 3+4: Wire react-hook-form+zod into all form pages; disable buttons during submit 2026-05-02 17:09:31 -05:00
Spencer Twaddle 95665e5baa Phase 2: Create zod schemas in src/schemas/index.ts 2026-05-02 17:01:55 -05:00
Spencer Twaddle afeff4b308 Phase 1: Install react-hook-form, zod, @hookform/resolvers 2026-05-02 17:01:21 -05:00
Spencer Twaddle ae28abdb3e Phase 12: Add TanStack React Query
- Install @tanstack/react-query
- Create queryClient with MutationCache for global toast on success/error
- QueryToastBridge component bridges module-level handlers to ToastProvider
- Wrap App in QueryClientProvider
- Create domain hook files: budgets, incomes, outgos, shares, summary
- Update all 5 pages to use hooks; remove inline useEffect/useState fetch logic
- DnD reorder pages use local displayItems state synced from query data
2026-05-02 16:48:00 -05:00
Spencer Twaddle b33ff5079c Replace custom AuthContext with react-oidc-context
Deletes the hand-rolled AuthContext/UserManager setup and replaces it
with AuthProvider from react-oidc-context. onSigninCallback clears the
OIDC code params from the URL (unless an error is present). TokenSync
bridges the library token into the existing api/client setTokenProvider
pattern. AuthGuard updated to use auth.isLoading/isAuthenticated/
signinRedirect from the library. CallbackPage simplified to a passive
error renderer — react-oidc-context processes the OIDC exchange itself.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 16:38:12 -05:00
Spencer Twaddle 65701cbdb8 Add react-oidc-context dependency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 16:37:01 -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 c3d1420c4c Add docker-compose.yml following stwaddle stack pattern
Joins web (nginx-proxy), apps-internal (database), and auth-public
(OIDC discovery without hairpinning) networks. External networks are
pre-created by the stwaddle infra compose; apps-db is defined here
so the budget service can be deployed standalone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 15:58:06 -05:00
Spencer Twaddle 4dc5ad4910 Rework client OIDC env vars: rename to VITE_OIDC_*, add committed .env
Renames VITE_AUTH_* to VITE_OIDC_* to match the stack convention.
Adds a dedicated VITE_OIDC_POST_LOGOUT_REDIRECT_URI instead of deriving
it from the redirect URI via string replace. Switches from Dockerfile
ARG/ENV to a committed src/Budget.Client/.env so Vite picks up
production values at build time without needing build-arg overrides.
.env.local is gitignored for localhost dev overrides.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 15:57:52 -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 a8cf6957b5 Add README and fix Dockerfile VITE build args
README covers all env vars, docker-compose/env examples, and full
auth server setup (scope, client registration, user roles).

Dockerfile now accepts VITE_AUTH_* build args with production defaults
so the values are baked into the client bundle correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 08:51:55 -05:00
Spencer Twaddle 9941ecc1a9 Add Docker build and push script
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 08:11:25 -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 64203606a6 Phase 7: Budget list, settings, and sharing UI
- Implement BudgetsPage: list all budgets, create new budget, navigate to income view
- Implement SettingsPage: rename budget, edit tax rate, manage shares table
- Shares table shows permission dropdowns, pending status badge, revoke button
- Add share form: email input, permission selector, add button
- BudgetNav component used across all budget pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 07:59:40 -05:00
Spencer Twaddle 69d6ac0bea Phase 6: Summary API and page
- Add SummaryController: computes monthly income, breakdown by type (Need/Want/Save/Unspent), and pre-tax income
- Need/Want/Save get target% (50/30/20), maxAmount, and remaining; Unspent shows totals only
- PUT /summary/tax-rate updates EffectiveTaxRate on the budget (no new migration needed)
- Add SummaryDto, SummaryBreakdownItem, PreTaxIncomeDto DTOs
- Add Summary page: income header cards, type breakdown table with ⓘ tooltip for target%,
  pre-tax section with editable tax rate field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 07:58:54 -05:00
Spencer Twaddle 38296bc22a Phase 5: Outgo API and page
- Add OutgosController: list, create, update, delete, reorder, categories, payment-sources endpoints
- Outgo DTOs with computed Monthly, Annually, MonthlyPercent fields
- Add AutocompleteInput component with filtered suggestion dropdown
- Add Outgo page: full table with all columns, inline editing, add/delete, drag-to-reorder
- Autocomplete wired to categories and payment-sources API endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 07:57:52 -05:00
Spencer Twaddle f429a747d8 Phase 4: Income API and page
- Add FrequencyCalculator static utility (all 9 frequency multipliers)
- Add IncomesController: list, create, update, delete, reorder
- Add Income DTOs with computed Monthly/Annually fields
- Add shared TypeScript types (IncomeDto, OutgoDto, BudgetDto, ShareDto, SummaryDto)
- Add API client with Bearer token injection via setTokenProvider
- Add FrequencySelect, MoneyDisplay, BudgetNav shared components
- Add Income page: sortable table with inline editing, add/delete rows, drag-to-reorder via dnd-kit
- Wire TokenWirer in App.tsx to keep API client in sync with auth state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 07:56:42 -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