259 lines
8.8 KiB
C#
259 lines
8.8 KiB
C#
using System.Security.Claims;
|
|
using GmRelay.Web.Services.Portfolio.Covers;
|
|
|
|
namespace GmRelay.Web.Services.Portfolio;
|
|
|
|
public sealed class AuthorizedPortfolioService(
|
|
IPortfolioStore portfolioStore,
|
|
ISessionStore sessionStore,
|
|
IPortfolioCoverStorage coverStorage,
|
|
IHttpContextAccessor httpContextAccessor)
|
|
{
|
|
private (string Platform, string ExternalUserId, string? Name)? GetCurrentIdentity()
|
|
{
|
|
var user = httpContextAccessor.HttpContext?.User;
|
|
if (user is null || !user.TryGetPlatformIdentity(out var platform, out var externalUserId))
|
|
return null;
|
|
|
|
var name = user.FindFirst(ClaimTypes.Name)?.Value;
|
|
return (platform, externalUserId, name);
|
|
}
|
|
|
|
private async Task<(string Platform, string ExternalUserId)> RequireManagerAsync(Guid groupId)
|
|
{
|
|
var identity = GetCurrentIdentity();
|
|
if (identity is null)
|
|
{
|
|
throw new SessionAccessDeniedException(groupId, "<anonymous>");
|
|
}
|
|
|
|
if (!await sessionStore.IsGroupManagerAsync(groupId, identity.Value.Platform, identity.Value.ExternalUserId))
|
|
{
|
|
throw new SessionAccessDeniedException(groupId, identity.Value.ExternalUserId);
|
|
}
|
|
|
|
return (identity.Value.Platform, identity.Value.ExternalUserId);
|
|
}
|
|
|
|
private async Task<(Guid GroupId, string Platform, string ExternalUserId)> RequireManagerForGameAsync(Guid portfolioGameId)
|
|
{
|
|
var identity = GetCurrentIdentity();
|
|
if (identity is null)
|
|
{
|
|
throw new SessionAccessDeniedException(portfolioGameId, "<anonymous>");
|
|
}
|
|
|
|
var groupId = await portfolioStore.GetPortfolioGameGroupIdAsync(portfolioGameId);
|
|
if (groupId is null)
|
|
{
|
|
throw new InvalidOperationException("Portfolio game not found.");
|
|
}
|
|
|
|
if (!await sessionStore.IsGroupManagerAsync(groupId.Value, identity.Value.Platform, identity.Value.ExternalUserId))
|
|
{
|
|
throw new SessionAccessDeniedException(portfolioGameId, identity.Value.ExternalUserId);
|
|
}
|
|
|
|
return (groupId.Value, identity.Value.Platform, identity.Value.ExternalUserId);
|
|
}
|
|
|
|
// --- Protected reads ---
|
|
|
|
public async Task<IReadOnlyList<PortfolioGameSummary>> GetPortfolioGamesForCurrentUserAsync(Guid groupId)
|
|
{
|
|
var identity = GetCurrentIdentity();
|
|
if (identity is null)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
if (!await sessionStore.IsGroupManagerAsync(groupId, identity.Value.Platform, identity.Value.ExternalUserId))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
return await portfolioStore.GetPortfolioGamesForGroupAsync(groupId);
|
|
}
|
|
|
|
public async Task<PortfolioGameEditor?> GetPortfolioGameForCurrentUserAsync(Guid portfolioGameId)
|
|
{
|
|
var identity = GetCurrentIdentity();
|
|
if (identity is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var groupId = await portfolioStore.GetPortfolioGameGroupIdAsync(portfolioGameId);
|
|
if (groupId is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (!await sessionStore.IsGroupManagerAsync(groupId.Value, identity.Value.Platform, identity.Value.ExternalUserId))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return await portfolioStore.GetPortfolioGameForManagementAsync(portfolioGameId);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<PortfolioSessionOption>> GetCompletedSessionsForCurrentUserAsync(Guid groupId)
|
|
{
|
|
var identity = GetCurrentIdentity();
|
|
if (identity is null)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
if (!await sessionStore.IsGroupManagerAsync(groupId, identity.Value.Platform, identity.Value.ExternalUserId))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
return await portfolioStore.GetEligibleCompletedSessionsAsync(groupId, null);
|
|
}
|
|
|
|
// --- Protected writes ---
|
|
|
|
public async Task<Guid> CreateDraftForCurrentUserAsync(Guid groupId, Guid? preselectedSessionId)
|
|
{
|
|
await RequireManagerAsync(groupId);
|
|
return await portfolioStore.CreatePortfolioDraftAsync(groupId, preselectedSessionId);
|
|
}
|
|
|
|
public async Task UpdateDraftForCurrentUserAsync(Guid portfolioGameId, PortfolioGameUpdate update)
|
|
{
|
|
var (groupId, _, _) = await RequireManagerForGameAsync(portfolioGameId);
|
|
|
|
var normalized = NormalizeUpdate(update);
|
|
await portfolioStore.UpdatePortfolioDraftAsync(portfolioGameId, groupId, normalized);
|
|
}
|
|
|
|
public async Task ReplaceCoverForCurrentUserAsync(
|
|
Guid portfolioGameId,
|
|
Stream content,
|
|
string contentType,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var (groupId, _, _) = await RequireManagerForGameAsync(portfolioGameId);
|
|
|
|
var saveResult = await coverStorage.SaveAsync(content, contentType, cancellationToken);
|
|
var newKey = saveResult.StorageKey;
|
|
|
|
try
|
|
{
|
|
var oldKey = await portfolioStore.SetPortfolioCoverAsync(portfolioGameId, groupId, newKey);
|
|
if (!string.IsNullOrWhiteSpace(oldKey))
|
|
{
|
|
await coverStorage.DeleteIfExistsAsync(oldKey, cancellationToken);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
await coverStorage.DeleteIfExistsAsync(newKey, cancellationToken);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task DeleteForCurrentUserAsync(Guid portfolioGameId)
|
|
{
|
|
var (groupId, _, _) = await RequireManagerForGameAsync(portfolioGameId);
|
|
|
|
var coverKey = await portfolioStore.DeletePortfolioGameAsync(portfolioGameId, groupId);
|
|
if (!string.IsNullOrWhiteSpace(coverKey))
|
|
{
|
|
await coverStorage.DeleteIfExistsAsync(coverKey);
|
|
}
|
|
}
|
|
|
|
public async Task SetPublicationForCurrentUserAsync(Guid portfolioGameId, bool isPublic)
|
|
{
|
|
var (groupId, _, _) = await RequireManagerForGameAsync(portfolioGameId);
|
|
await portfolioStore.SetPortfolioPublicationAsync(portfolioGameId, groupId, isPublic);
|
|
}
|
|
|
|
public async Task ModerateReviewForCurrentUserAsync(
|
|
Guid portfolioGameId,
|
|
Guid reviewId,
|
|
string moderationStatus)
|
|
{
|
|
var (groupId, platform, externalUserId) = await RequireManagerForGameAsync(portfolioGameId);
|
|
|
|
var moderatorPlayerId = await sessionStore.ResolveEffectivePlayerIdAsync(platform, externalUserId);
|
|
if (moderatorPlayerId is null)
|
|
{
|
|
throw new InvalidOperationException("Authenticated player not found.");
|
|
}
|
|
|
|
await portfolioStore.ModeratePortfolioReviewAsync(
|
|
reviewId,
|
|
portfolioGameId,
|
|
groupId,
|
|
moderatorPlayerId.Value,
|
|
moderationStatus);
|
|
}
|
|
|
|
// --- Review submission ---
|
|
|
|
public async Task<PortfolioReviewSubmissionState> GetReviewSubmissionStateForCurrentUserAsync(string slug)
|
|
{
|
|
var identity = GetCurrentIdentity();
|
|
if (identity is null)
|
|
{
|
|
return PortfolioReviewSubmissionState.RequiresAuthentication;
|
|
}
|
|
|
|
return await portfolioStore.GetReviewSubmissionStateAsync(slug, identity.Value.Platform, identity.Value.ExternalUserId);
|
|
}
|
|
|
|
public async Task SubmitReviewForCurrentUserAsync(string slug, string body, bool publicationConsent)
|
|
{
|
|
if (!publicationConsent)
|
|
{
|
|
throw new InvalidOperationException("Public review requires explicit consent.");
|
|
}
|
|
|
|
var identity = GetCurrentIdentity();
|
|
if (identity is null)
|
|
{
|
|
throw new SessionAccessDeniedException(Guid.Empty, "<anonymous>");
|
|
}
|
|
|
|
var normalizedSlug = PortfolioValidation.NormalizeSlug(slug);
|
|
var normalizedBody = PortfolioValidation.NormalizeReviewBody(body);
|
|
|
|
var displayName = identity.Value.Name?.Trim() ?? identity.Value.ExternalUserId;
|
|
if (displayName.Length == 0)
|
|
{
|
|
throw new InvalidOperationException("Display name is required.");
|
|
}
|
|
|
|
await portfolioStore.SubmitPortfolioReviewAsync(
|
|
normalizedSlug,
|
|
identity.Value.Platform,
|
|
identity.Value.ExternalUserId,
|
|
displayName,
|
|
normalizedBody);
|
|
}
|
|
|
|
// --- Internal helpers ---
|
|
|
|
private static PortfolioGameUpdate NormalizeUpdate(PortfolioGameUpdate update)
|
|
{
|
|
var title = PortfolioValidation.NormalizeTitle(update.Title);
|
|
var slug = string.IsNullOrWhiteSpace(update.PublicSlug) ? null : PortfolioValidation.NormalizeSlug(update.PublicSlug);
|
|
var description = PortfolioValidation.NormalizeDescription(update.Description);
|
|
var format = PortfolioValidation.NormalizeFormat(update.Format);
|
|
var system = string.IsNullOrWhiteSpace(update.System) ? null : update.System.Trim();
|
|
|
|
return update with
|
|
{
|
|
Title = title,
|
|
PublicSlug = slug,
|
|
Description = description,
|
|
System = system,
|
|
Format = format
|
|
};
|
|
}
|
|
}
|