- 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
124 lines
4.7 KiB
C#
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;
|
|
}
|
|
|
|
|