< Summary

Information
Class: Chronicis.Api.Infrastructure.CurrentUserService
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Infrastructure/CurrentUserService.cs
Line coverage
100%
Covered lines: 24
Uncovered lines: 0
Coverable lines: 24
Total lines: 137
Line coverage: 100%
Branch coverage
100%
Covered branches: 26
Total branches: 26
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_IsAuthenticated()100%66100%
GetAuth0UserId()100%1212100%
ExtractNameFromEmail(...)100%88100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/Infrastructure/CurrentUserService.cs

#LineLine coverage
 1using System.Security.Claims;
 2using Chronicis.Api.Services;
 3using Chronicis.Shared.Admin;
 4using Chronicis.Shared.Models;
 5
 6namespace Chronicis.Api.Infrastructure;
 7
 8/// <summary>
 9/// Implementation of ICurrentUserService that resolves the user from HTTP context claims.
 10/// This service is scoped per-request and caches the user lookup for the request lifetime.
 11/// </summary>
 12public class CurrentUserService : ICurrentUserService
 13{
 14    private readonly IHttpContextAccessor _httpContextAccessor;
 15    private readonly IUserService _userService;
 16    private readonly ISysAdminChecker _sysAdminChecker;
 17    private User? _cachedUser;
 18    private bool _userLookedUp;
 19
 20    public CurrentUserService(
 21        IHttpContextAccessor httpContextAccessor,
 22        IUserService userService,
 23        ISysAdminChecker sysAdminChecker)
 24    {
 3225        _httpContextAccessor = httpContextAccessor;
 3226        _userService = userService;
 3227        _sysAdminChecker = sysAdminChecker;
 3228    }
 29
 30    public bool IsAuthenticated =>
 3331        _httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
 32
 33    public string? GetAuth0UserId()
 34    {
 3035        var user = _httpContextAccessor.HttpContext?.User;
 3036        if (user == null || !IsAuthenticated)
 637            return null;
 38
 39        // Auth0 puts the user ID in the 'sub' claim (NameIdentifier)
 2440        return user.FindFirst(ClaimTypes.NameIdentifier)?.Value
 2441            ?? user.FindFirst("sub")?.Value;
 42    }
 43
 44    public async Task<User?> GetCurrentUserAsync()
 45    {
 46        // Return cached user if already looked up this request
 47        if (_userLookedUp)
 48            return _cachedUser;
 49
 50        _userLookedUp = true;
 51
 52        var auth0UserId = GetAuth0UserId();
 53        if (string.IsNullOrEmpty(auth0UserId))
 54            return null;
 55
 56        // Extract additional claims for user creation/update
 57        var claimsPrincipal = _httpContextAccessor.HttpContext?.User;
 58
 59        const string customNamespace = "https://chronicis.app";
 60
 61        var email = claimsPrincipal?.FindFirst($"{customNamespace}/email")?.Value
 62                   ?? claimsPrincipal?.FindFirst(ClaimTypes.Email)?.Value
 63                   ?? claimsPrincipal?.FindFirst("email")?.Value
 64                   ?? "";
 65
 66        var displayName = claimsPrincipal?.FindFirst($"{customNamespace}/name")?.Value
 67                         ?? claimsPrincipal?.FindFirst(ClaimTypes.Name)?.Value
 68                         ?? claimsPrincipal?.FindFirst("name")?.Value
 69                         ?? claimsPrincipal?.FindFirst("nickname")?.Value
 70                         ?? claimsPrincipal?.FindFirst("preferred_username")?.Value
 71                         ?? claimsPrincipal?.FindFirst("given_name")?.Value
 72                         ?? ExtractNameFromEmail(email)
 73                         ?? "Unknown User";
 74
 75        var avatarUrl = claimsPrincipal?.FindFirst($"{customNamespace}/picture")?.Value
 76                       ?? claimsPrincipal?.FindFirst("picture")?.Value;
 77
 78        // Get or create the user in the database
 79        _cachedUser = await _userService.GetOrCreateUserAsync(
 80            auth0UserId,
 81            email,
 82            displayName,
 83            avatarUrl);
 84
 85        return _cachedUser;
 86    }
 87
 88    public async Task<User> GetRequiredUserAsync()
 89    {
 90        var user = await GetCurrentUserAsync();
 91        return user ?? throw new InvalidOperationException(
 92            "User not found. Ensure this endpoint requires authentication.");
 93    }
 94
 95    /// <inheritdoc/>
 96    public async Task<bool> IsSysAdminAsync()
 97    {
 98        var auth0UserId = GetAuth0UserId();
 99        if (string.IsNullOrEmpty(auth0UserId))
 100            return false;
 101
 102        // Resolve email from the cached/current user to support email-based sysadmin checks.
 103        var user = await GetCurrentUserAsync();
 104        return _sysAdminChecker.IsSysAdmin(auth0UserId, user?.Email);
 105    }
 106
 107    /// <summary>
 108    /// Extracts a display name from an email address as a fallback.
 109    /// e.g., "john.doe@example.com" becomes "John Doe"
 110    /// </summary>
 111    private static string? ExtractNameFromEmail(string? email)
 112    {
 7113        if (string.IsNullOrEmpty(email) || !email.Contains('@'))
 3114            return null;
 115
 4116        var localPart = email.Split('@')[0];
 117
 118        // Replace common separators with spaces
 4119        var name = localPart
 4120            .Replace('.', ' ')
 4121            .Replace('_', ' ')
 4122            .Replace('-', ' ');
 123
 124        // Title case each word
 4125        var words = name.Split(' ', StringSplitOptions.RemoveEmptyEntries);
 4126        var titleCased = words.Select(w =>
 4127            char.ToUpper(w[0]) + (w.Length > 1 ? w[1..].ToLowerInvariant() : ""));
 128
 4129        var result = string.Join(" ", titleCased);
 130
 131        // Don't return if it looks like gibberish (all numbers, too short, etc.)
 4132        if (result.Length < 2 || result.All(c => char.IsDigit(c)))
 2133            return null;
 134
 2135        return result;
 136    }
 137}