using Budget.Api.Data; using Budget.Api.DTOs; using Budget.Api.Models; using Budget.Api.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; namespace Budget.Api.Controllers; [ApiController] [Route("api/budgets/{budgetId:guid}/shares")] [Authorize] public class SharesController(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 List(Guid budgetId) { if (TryGetUserId(out var userId) is { } err) return err; if (!await authz.CanReadAsync(budgetId, userId)) return Forbid(); var shares = await db.BudgetShares .Where(s => s.BudgetId == budgetId) .Select(s => new ShareDto(s.Id, s.SharedWithEmail, s.Permission, s.IsPending, s.CreatedAt)) .ToListAsync(); return Ok(shares); } [HttpPost] [EnableRateLimiting("writes")] public async Task Add(Guid budgetId, [FromBody] CreateShareRequest req) { if (TryGetUserId(out var userId) is { } err) return err; if (!await authz.IsOwnerAsync(budgetId, userId)) return Forbid(); var existing = await db.BudgetShares .FirstOrDefaultAsync(s => s.BudgetId == budgetId && s.SharedWithEmail == req.Email); if (existing is not null) return Conflict("A share for that email already exists."); var knownUser = await db.KnownUsers.FirstOrDefaultAsync(u => u.Email == req.Email); var share = new BudgetShare { Id = Guid.NewGuid(), BudgetId = budgetId, SharedWithEmail = req.Email, SharedWithUserId = knownUser?.Id, IsPending = knownUser is null, Permission = req.Permission, CreatedAt = DateTimeOffset.UtcNow, }; db.BudgetShares.Add(share); await db.SaveChangesAsync(); return Ok(new ShareDto(share.Id, share.SharedWithEmail, share.Permission, share.IsPending, share.CreatedAt)); } [HttpPut("{shareId:guid}")] [EnableRateLimiting("writes")] public async Task Update(Guid budgetId, Guid shareId, [FromBody] UpdateShareRequest req) { if (TryGetUserId(out var userId) is { } err) return err; if (!await authz.IsOwnerAsync(budgetId, userId)) return Forbid(); var share = await db.BudgetShares.FirstOrDefaultAsync(s => s.Id == shareId && s.BudgetId == budgetId); if (share is null) return NotFound(); share.Permission = req.Permission; await db.SaveChangesAsync(); return Ok(new ShareDto(share.Id, share.SharedWithEmail, share.Permission, share.IsPending, share.CreatedAt)); } [HttpDelete("{shareId:guid}")] [EnableRateLimiting("writes")] public async Task Revoke(Guid budgetId, Guid shareId) { if (TryGetUserId(out var userId) is { } err) return err; if (!await authz.IsOwnerAsync(budgetId, userId)) return Forbid(); var share = await db.BudgetShares.FirstOrDefaultAsync(s => s.Id == shareId && s.BudgetId == budgetId); if (share is null) return NotFound(); db.BudgetShares.Remove(share); await db.SaveChangesAsync(); return NoContent(); } }