using Books.Api.Domain.UserAccess; using Books.Api.EventFlow.Repositories; namespace Books.Api.Authorization; /// /// Service for checking company access permissions. /// public interface ICompanyAccessService { /// /// Check if the current user has access to the specified company with the required role. /// Throws UnauthorizedAccessException if access is denied. /// Task RequireAccessAsync( string companyId, CompanyRole minimumRole, CancellationToken cancellationToken = default); /// /// Check if the current user has access to the specified company. /// Returns the access DTO if granted, null if denied. /// Task GetAccessAsync( string companyId, CancellationToken cancellationToken = default); /// /// Get all companies the current user has access to. /// Task> GetUserCompaniesAsync( CancellationToken cancellationToken = default); /// /// Check if the current user has write access (Owner or Accountant) to the company. /// Task CanWriteAsync( string companyId, CancellationToken cancellationToken = default); /// /// Check if the current user can manage users for the company (Owner only). /// Task CanManageUsersAsync( string companyId, CancellationToken cancellationToken = default); } public class CompanyAccessService( IHttpContextAccessor httpContextAccessor, IUserCompanyAccessRepository accessRepository) : ICompanyAccessService { public async Task RequireAccessAsync( string companyId, CompanyRole minimumRole, CancellationToken cancellationToken = default) { var userId = GetCurrentUserId(); if (userId == null) { throw new UnauthorizedAccessException("User is not authenticated"); } var hasAccess = await accessRepository.HasAccessAsync(userId, companyId, minimumRole, cancellationToken); if (!hasAccess) { var roleDescription = minimumRole switch { CompanyRole.Owner => "ejer", CompanyRole.Accountant => "bogholder", CompanyRole.Viewer => "læser", _ => "ukendt" }; throw new Domain.DomainException( "ACCESS_DENIED", $"You do not have {minimumRole} access to this company", $"Du har ikke {roleDescription}-adgang til denne virksomhed"); } } public async Task GetAccessAsync( string companyId, CancellationToken cancellationToken = default) { var userId = GetCurrentUserId(); if (userId == null) return null; return await accessRepository.GetAccessAsync(userId, companyId, cancellationToken); } public async Task> GetUserCompaniesAsync( CancellationToken cancellationToken = default) { var userId = GetCurrentUserId(); if (userId == null) return []; return await accessRepository.GetByUserIdAsync(userId, cancellationToken); } public async Task CanWriteAsync( string companyId, CancellationToken cancellationToken = default) { var userId = GetCurrentUserId(); if (userId == null) return false; return await accessRepository.HasAccessAsync(userId, companyId, CompanyRole.Accountant, cancellationToken); } public async Task CanManageUsersAsync( string companyId, CancellationToken cancellationToken = default) { var userId = GetCurrentUserId(); if (userId == null) return false; return await accessRepository.HasAccessAsync(userId, companyId, CompanyRole.Owner, cancellationToken); } private string? GetCurrentUserId() { var user = httpContextAccessor.HttpContext?.User; if (user?.Identity?.IsAuthenticated != true) return null; // For API keys, use the API key ID as the user ID // For OIDC users, use the NameIdentifier claim (Keycloak user ID) return user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; } }