087fbdd176
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>
95 lines
3.3 KiB
C#
95 lines
3.3 KiB
C#
using Budget.Api.Services;
|
|
using Budget.Core.DTOs;
|
|
using Budget.Core.Models;
|
|
using Budget.Infrastructure.Data;
|
|
using Budget.Infrastructure.Services;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.RateLimiting;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using BudgetEntity = Budget.Core.Models.Budget;
|
|
|
|
namespace Budget.Api.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
[Authorize]
|
|
public class BudgetsController(AppDbContext db, BudgetAuthorizationService authz) : ControllerBase
|
|
{
|
|
private IActionResult? TryGetUserId(out string userId)
|
|
{
|
|
var id = User.GetUserId();
|
|
if (id is null) { userId = string.Empty; return Unauthorized(); }
|
|
userId = id;
|
|
return null;
|
|
}
|
|
|
|
[HttpGet]
|
|
public async Task<IActionResult> List()
|
|
{
|
|
if (TryGetUserId(out var userId) is { } err) return err;
|
|
var budgets = await db.Budgets
|
|
.Where(b => b.OwnerUserId == userId ||
|
|
b.Shares.Any(s => s.SharedWithUserId == userId && !s.IsPending))
|
|
.Select(b => new BudgetDto(b.Id, b.Name, b.EffectiveTaxRate, b.CreatedAt, b.UpdatedAt))
|
|
.ToListAsync();
|
|
return Ok(budgets);
|
|
}
|
|
|
|
[HttpPost]
|
|
[EnableRateLimiting("writes")]
|
|
public async Task<IActionResult> Create([FromBody] CreateBudgetRequest req)
|
|
{
|
|
if (TryGetUserId(out var userId) is { } err) return err;
|
|
var budget = new BudgetEntity
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = req.Name,
|
|
OwnerUserId = userId,
|
|
CreatedAt = DateTimeOffset.UtcNow,
|
|
UpdatedAt = DateTimeOffset.UtcNow,
|
|
};
|
|
db.Budgets.Add(budget);
|
|
await db.SaveChangesAsync();
|
|
return CreatedAtAction(nameof(Get), new { id = budget.Id },
|
|
new BudgetDto(budget.Id, budget.Name, budget.EffectiveTaxRate, budget.CreatedAt, budget.UpdatedAt));
|
|
}
|
|
|
|
[HttpGet("{id:guid}")]
|
|
public async Task<IActionResult> Get(Guid id)
|
|
{
|
|
if (TryGetUserId(out var userId) is { } err) return err;
|
|
if (!await authz.CanReadAsync(id, userId)) return Forbid();
|
|
var b = await db.Budgets.FindAsync(id);
|
|
if (b is null) return NotFound();
|
|
return Ok(new BudgetDto(b.Id, b.Name, b.EffectiveTaxRate, b.CreatedAt, b.UpdatedAt));
|
|
}
|
|
|
|
[HttpPut("{id:guid}")]
|
|
[EnableRateLimiting("writes")]
|
|
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateBudgetRequest req)
|
|
{
|
|
if (TryGetUserId(out var userId) is { } err) return err;
|
|
if (!await authz.CanWriteAsync(id, userId)) return Forbid();
|
|
var b = await db.Budgets.FindAsync(id);
|
|
if (b is null) return NotFound();
|
|
b.Name = req.Name;
|
|
b.UpdatedAt = DateTimeOffset.UtcNow;
|
|
await db.SaveChangesAsync();
|
|
return Ok(new BudgetDto(b.Id, b.Name, b.EffectiveTaxRate, b.CreatedAt, b.UpdatedAt));
|
|
}
|
|
|
|
[HttpDelete("{id:guid}")]
|
|
[EnableRateLimiting("writes")]
|
|
public async Task<IActionResult> Delete(Guid id)
|
|
{
|
|
if (TryGetUserId(out var userId) is { } err) return err;
|
|
if (!await authz.IsOwnerAsync(id, userId)) return Forbid();
|
|
var b = await db.Budgets.FindAsync(id);
|
|
if (b is null) return NotFound();
|
|
db.Budgets.Remove(b);
|
|
await db.SaveChangesAsync();
|
|
return NoContent();
|
|
}
|
|
}
|