proposal-system/api/src/ProposalSystem.Api/Services/CurrentUserService.cs
Adam Moussa 184bc1da7e Fix Phase 2 audit findings: reliability, UX, and operational monitoring
BLOCK-10: Add CloudWatch alarms (DLQ, Lambda errors, RDS, API 5xx) with SNS email
BLOCK-11: Remove sync-over-async deadlock in CurrentUserService
BLOCK-12: Add AppDelegate OAuth URL callback handler for mobile
BLOCK-13: Wire mobile 401 interceptor to dispatch Redux logout
BLOCK-14: Fix JWT base64 padding crash and SysAdmin role detection
BLOCK-15: Reset pagination to page 1 on filter change
BLOCK-16: Add unsaved-changes guard (beforeunload + useBlocker) to AdminWorkspace
FIX-08: Add BulkUpdateLineItems FluentValidation validator
FIX-13: Display auth errors on LoginPage
FIX-25: Add token refresh with retry queue to mobile API client
FIX-44: Add httpx retry logic to all Lambda handlers
FIX-42/43: Align docker-compose PG version (15) and DB name (proposals) with RDS
2026-05-20 19:07:49 -04:00

101 lines
3.5 KiB
C#

using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using ProposalSystem.Application.Interfaces;
using ProposalSystem.Domain.Entities;
using ProposalSystem.Infrastructure.Data;
namespace ProposalSystem.Api.Services;
public class CurrentUserService : ICurrentUserService
{
private readonly IHttpContextAccessor _httpContext;
private readonly ProposalDbContext _db;
private User? _cachedUser;
public CurrentUserService(IHttpContextAccessor httpContext, ProposalDbContext db)
{
_httpContext = httpContext;
_db = db;
}
public Guid UserId => GetOrThrow().Id;
public string Email => GetOrThrow().Email;
public UserRole Role => GetOrThrow().Role;
public string? IpAddress =>
_httpContext.HttpContext?.Connection.RemoteIpAddress?.ToString();
public async Task ResolveAsync()
{
if (_cachedUser != null) return;
var principal = _httpContext.HttpContext?.User
?? throw new UnauthorizedAccessException("No authenticated user");
var sub = principal.FindFirstValue("sub");
var email = principal.FindFirstValue(ClaimTypes.Email)
?? principal.FindFirstValue("email")
?? "unknown@seahaven.com";
var name = principal.FindFirstValue("name")
?? email.Split('@')[0];
var groups = principal.FindAll("cognito:groups")
.Select(c => c.Value).ToList();
var role = groups.Contains("sysadmins") ? UserRole.SysAdmin
: groups.Contains("admins") ? UserRole.Admin
: UserRole.Dispatcher;
var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId != null && Guid.TryParse(userId, out var parsedId))
_cachedUser = await _db.Users.FirstOrDefaultAsync(u => u.Id == parsedId);
if (_cachedUser == null && sub != null)
_cachedUser = await _db.Users.FirstOrDefaultAsync(u => u.CognitoSub == sub);
if (_cachedUser == null)
_cachedUser = await _db.Users.FirstOrDefaultAsync(u => u.Email == email);
if (_cachedUser != null)
{
var changed = false;
if (_cachedUser.Email != email) { _cachedUser.Email = email; changed = true; }
if (_cachedUser.DisplayName != name) { _cachedUser.DisplayName = name; changed = true; }
if (changed)
{
_cachedUser.UpdatedAt = DateTime.UtcNow;
await _db.SaveChangesAsync();
}
return;
}
_cachedUser = new User
{
Id = Guid.NewGuid(),
CognitoSub = sub ?? $"auto-{Guid.NewGuid():N}",
Email = email,
DisplayName = name,
Role = role,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
};
_db.Users.Add(_cachedUser);
try
{
await _db.SaveChangesAsync();
}
catch (DbUpdateException)
{
_db.Entry(_cachedUser).State = EntityState.Detached;
_cachedUser = await _db.Users.FirstOrDefaultAsync(u => u.Email == email)
?? throw new UnauthorizedAccessException("Could not resolve current user");
}
}
private User GetOrThrow()
{
return _cachedUser
?? throw new InvalidOperationException(
"CurrentUserService.ResolveAsync() was not called. Ensure the authentication middleware runs before accessing user properties.");
}
}