< 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
0%
Covered lines: 0
Uncovered lines: 58
Coverable lines: 58
Total lines: 121
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 102
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_IsAuthenticated()0%4260%
GetAuth0UserId()0%156120%
GetCurrentUserAsync()0%5256720%
GetRequiredUserAsync()0%620%
ExtractNameFromEmail(...)0%110100%

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.Models;
 4
 5namespace Chronicis.Api.Infrastructure;
 6
 7/// <summary>
 8/// Implementation of ICurrentUserService that resolves the user from HTTP context claims.
 9/// This service is scoped per-request and caches the user lookup for the request lifetime.
 10/// </summary>
 11public class CurrentUserService : ICurrentUserService
 12{
 13    private readonly IHttpContextAccessor _httpContextAccessor;
 14    private readonly IUserService _userService;
 15    private User? _cachedUser;
 16    private bool _userLookedUp;
 17
 018    public CurrentUserService(
 019        IHttpContextAccessor httpContextAccessor,
 020        IUserService userService)
 21    {
 022        _httpContextAccessor = httpContextAccessor;
 023        _userService = userService;
 024    }
 25
 26    public bool IsAuthenticated =>
 027        _httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
 28
 29    public string? GetAuth0UserId()
 30    {
 031        var user = _httpContextAccessor.HttpContext?.User;
 032        if (user == null || !IsAuthenticated)
 033            return null;
 34
 35        // Auth0 puts the user ID in the 'sub' claim (NameIdentifier)
 036        return user.FindFirst(ClaimTypes.NameIdentifier)?.Value
 037            ?? user.FindFirst("sub")?.Value;
 38    }
 39
 40    public async Task<User?> GetCurrentUserAsync()
 41    {
 42        // Return cached user if already looked up this request
 043        if (_userLookedUp)
 044            return _cachedUser;
 45
 046        _userLookedUp = true;
 47
 048        var auth0UserId = GetAuth0UserId();
 049        if (string.IsNullOrEmpty(auth0UserId))
 050            return null;
 51
 52        // Extract additional claims for user creation/update
 053        var claimsPrincipal = _httpContextAccessor.HttpContext?.User;
 54
 55        const string customNamespace = "https://chronicis.app";
 56
 057        var email = claimsPrincipal?.FindFirst($"{customNamespace}/email")?.Value
 058                   ?? claimsPrincipal?.FindFirst(ClaimTypes.Email)?.Value
 059                   ?? claimsPrincipal?.FindFirst("email")?.Value
 060                   ?? "";
 61
 062        var displayName = claimsPrincipal?.FindFirst($"{customNamespace}/name")?.Value
 063                         ?? claimsPrincipal?.FindFirst(ClaimTypes.Name)?.Value
 064                         ?? claimsPrincipal?.FindFirst("name")?.Value
 065                         ?? claimsPrincipal?.FindFirst("nickname")?.Value
 066                         ?? claimsPrincipal?.FindFirst("preferred_username")?.Value
 067                         ?? claimsPrincipal?.FindFirst("given_name")?.Value
 068                         ?? ExtractNameFromEmail(email)
 069                         ?? "Unknown User";
 70
 071        var avatarUrl = claimsPrincipal?.FindFirst($"{customNamespace}/picture")?.Value
 072                       ?? claimsPrincipal?.FindFirst("picture")?.Value;
 73
 74        // Get or create the user in the database
 075        _cachedUser = await _userService.GetOrCreateUserAsync(
 076            auth0UserId,
 077            email,
 078            displayName,
 079            avatarUrl);
 80
 081        return _cachedUser;
 082    }
 83
 84    public async Task<User> GetRequiredUserAsync()
 85    {
 086        var user = await GetCurrentUserAsync();
 087        return user ?? throw new InvalidOperationException(
 088            "User not found. Ensure this endpoint requires authentication.");
 089    }
 90
 91    /// <summary>
 92    /// Extracts a display name from an email address as a fallback.
 93    /// e.g., "john.doe@example.com" becomes "John Doe"
 94    /// </summary>
 95    private static string? ExtractNameFromEmail(string? email)
 96    {
 097        if (string.IsNullOrEmpty(email) || !email.Contains('@'))
 098            return null;
 99
 0100        var localPart = email.Split('@')[0];
 101
 102        // Replace common separators with spaces
 0103        var name = localPart
 0104            .Replace('.', ' ')
 0105            .Replace('_', ' ')
 0106            .Replace('-', ' ');
 107
 108        // Title case each word
 0109        var words = name.Split(' ', StringSplitOptions.RemoveEmptyEntries);
 0110        var titleCased = words.Select(w =>
 0111            char.ToUpper(w[0]) + (w.Length > 1 ? w.Substring(1).ToLower() : ""));
 112
 0113        var result = string.Join(" ", titleCased);
 114
 115        // Don't return if it looks like gibberish (all numbers, too short, etc.)
 0116        if (result.Length < 2 || result.All(c => char.IsDigit(c)))
 0117            return null;
 118
 0119        return result;
 120    }
 121}