Files
MilkyShots/Lactose/Services/LactoseAuthService.cs
REDCODE 07fcd05031 feat: masonry gallery on homepage with infinite scroll
- Enrich AssetPreviewDto with ResolutionWidth/Height, HasThumbnail, HasPreview
- Extend GET /api/asset with ?type=&random=true for gallery queries
- Add general GetAssets repository method (type, dates, random, pagination)
- Fix LactoseAuthService.GetUserData to handle unmapped JWT claim names
- Add request-level auth debug logging middleware
- New AssetService (MilkStream.Client) for gallery API calls
- Rewrite Home.razor: CSS column-count masonry, infinite scroll via
  IntersectionObserver JS interop, inline preview overlay with
  progressive loading (preview -> thumbnail fallback), arrow/Esc nav
- Home.razor.css: responsive 2-5 column masonry, overlay styles,
  content-visibility:auto for memory optimization
- Sliding window of 10 pages (300 tiles) with loading=lazy images
- Broken thumbnail fallback to Bootstrap bi-image icon
2026-07-07 15:44:03 +02:00

124 lines
4.7 KiB
C#

using Butter.Types;
using Lactose.Configuration;
using Lactose.Models;
using Microsoft.Extensions.Options;
using System.Security.Claims;
namespace Lactose.Services;
/// <summary>
/// Handles authentication operations — token generation, user data extraction, and refresh token validation.
/// </summary>
public class LactoseAuthService(
ILogger<LactoseAuthService> logger,
ITokenService tokenService)
{
/// <summary>
/// The base expiration time for access tokens in minutes.
/// </summary>
public const int BaseExpirationTime = 10;
/// <summary>
/// The expiration time for refresh tokens in minutes.
/// </summary>
public const int LongExpirationTime = 60;
/// <summary>
/// Generates a JWT access token for the specified user.
/// </summary>
/// <param name="user">The user to generate the token for.</param>
/// <returns>A JWT access token string.</returns>
public string GenerateAccessToken(User user) => tokenService.GenerateToken(GenerateClaims(user), BaseExpirationTime);
/// <summary>
/// Generates a refresh token for the specified user and stores it.
/// </summary>
/// <param name="user">The user to generate the refresh token for.</param>
/// <returns>A refresh token string.</returns>
public string GenerateRefreshToken(User user) {
var token =tokenService.GenerateToken(LongExpirationTime);
user.RefreshToken = token;
user.RefreshTokenExpires = DateTime.Now.AddMinutes(LongExpirationTime);
return token;
}
static ClaimsIdentity GenerateClaims(User user) {
ClaimsIdentity claims = new ClaimsIdentity();
claims.AddClaim(new Claim(ClaimTypes.Email, user.Email));
claims.AddClaim(new Claim(ClaimTypes.Name, user.Username));
claims.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
claims.AddClaim(new Claim(ClaimTypes.Role, user.AccessLevel.ToString()));
return claims;
}
/// <summary>
/// Extracts the authenticated user data from the current HTTP context's claims principal.
/// Supports both short JWT claim names and mapped .NET claim types.
/// </summary>
/// <param name="user">The claims principal from the HTTP context.</param>
/// <returns>The authenticated user data, or null if the user is not authenticated.</returns>
public LactoseAuthenticatedUser? GetUserData(ClaimsPrincipal user) {
// Debug: log all claims to see what's actually present
var allClaims = string.Join(", ", user.Claims.Select(c => c.Type + "=" + c.Value));
logger.LogTrace("GetUserData claims: [{Claims}]", allClaims);
string? id = user.FindFirst(ClaimTypes.NameIdentifier)?.Value
?? user.FindFirst("nameid")?.Value;
if (id == null) return null;
string? name = user.FindFirst(ClaimTypes.Name)?.Value
?? user.FindFirst("unique_name")?.Value;
if (name == null) return null;
string? email = user.FindFirst(ClaimTypes.Email)?.Value
?? user.FindFirst("email")?.Value;
if (email == null) return null;
string? role = user.FindFirst(ClaimTypes.Role)?.Value
?? user.FindFirst("role")?.Value;
if (role == null) return null;
return new LactoseAuthenticatedUser(
Guid.Parse(id),
name,
email,
(EAccessLevel)Enum.Parse(typeof(EAccessLevel), role)
);
}
/// <summary>
/// Validates a refresh token against the user's stored refresh token.
/// </summary>
/// <param name="user">The user to validate against.</param>
/// <param name="tokenRefreshToken">The refresh token to validate.</param>
/// <returns>True if the refresh token is valid and not expired, false otherwise.</returns>
public bool ValidateRefreshToken(User user, string tokenRefreshToken) {
if (user.RefreshToken != tokenRefreshToken) return false;
if (user.RefreshTokenExpires < DateTime.Now) return false;
return true;
}
}
/// <summary>
/// Represents the authenticated user's extracted data from a JWT token.
/// </summary>
public sealed class LactoseAuthenticatedUser(Guid id, string username, string email, EAccessLevel accessLevel) {
/// <summary>
/// Gets the user ID.
/// </summary>
public Guid Id { get; } = id;
/// <summary>
/// Gets the username.
/// </summary>
public string Username { get; } = username;
/// <summary>
/// Gets the email address.
/// </summary>
public string Email { get; } = email;
/// <summary>
/// Gets the access level.
/// </summary>
public EAccessLevel AccessLevel { get; } = accessLevel;
}