Commit Graph

19 Commits

Author SHA1 Message Date
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