Files
budget/src/Budget.Api/Controllers/UsersController.cs
T
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

50 lines
1.4 KiB
C#

using Budget.Core.Models;
using Budget.Infrastructure.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Budget.Api.Controllers;
[ApiController]
[Route("api/users")]
[Authorize(Roles = "admin,user")]
public class UsersController(AppDbContext db) : ControllerBase
{
[HttpPost("me")]
public async Task<IActionResult> RegisterMe()
{
var sub = User.FindFirst("sub")?.Value;
var email = User.FindFirst("email")?.Value;
var name = User.FindFirst("name")?.Value;
if (sub is null || email is null || name is null)
return Unauthorized();
var known = await db.KnownUsers.FindAsync(sub);
if (known is null)
{
db.KnownUsers.Add(new KnownUser { Id = sub, Email = email, Name = name, LastSeenAt = DateTimeOffset.UtcNow });
}
else
{
known.Email = email;
known.Name = name;
known.LastSeenAt = DateTimeOffset.UtcNow;
}
var pending = await db.BudgetShares
.Where(s => s.IsPending && s.SharedWithEmail == email)
.ToListAsync();
foreach (var share in pending)
{
share.SharedWithUserId = sub;
share.IsPending = false;
}
await db.SaveChangesAsync();
return NoContent();
}
}