using System.Security.Claims; using GmRelay.Shared.Domain; namespace GmRelay.Web.Services; public sealed class AuthorizedSessionService(ISessionStore sessionStore, 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 ?? externalUserId; return (platform, externalUserId, name); } public Task> GetGroupsForCurrentUserAsync() { var identity = GetCurrentIdentity(); if (identity is null) return Task.FromResult(new List()); return sessionStore.GetGroupsForUserAsync(identity.Value.Platform, identity.Value.ExternalUserId); } public async Task GetGroupManagementForCurrentUserAsync(Guid groupId) { var identity = GetCurrentIdentity(); if (identity is null) return null; if (!await sessionStore.IsGroupManagerAsync(groupId, identity.Value.Platform, identity.Value.ExternalUserId)) return null; var group = await sessionStore.GetGroupAsync(groupId); if (group is null) return null; var managers = await sessionStore.GetGroupManagersAsync(groupId); var isOwner = await sessionStore.IsGroupOwnerAsync(groupId, identity.Value.Platform, identity.Value.ExternalUserId); return new WebGroupManagement(group, managers, isOwner); } public async Task?> GetUpcomingSessionsForCurrentUserAsync(Guid groupId) { var identity = GetCurrentIdentity(); if (identity is null) return null; if (!await sessionStore.IsGroupManagerAsync(groupId, identity.Value.Platform, identity.Value.ExternalUserId)) return null; return await sessionStore.GetUpcomingSessionsAsync(groupId); } public async Task GetSessionForCurrentUserAsync(Guid sessionId) { var identity = GetCurrentIdentity(); if (identity is null) return null; var session = await sessionStore.GetSessionAsync(sessionId); if (session is null) return null; return await sessionStore.IsGroupManagerAsync(session.GroupId, identity.Value.Platform, identity.Value.ExternalUserId) ? session : null; } public async Task GetBatchForCurrentUserAsync(Guid batchId) { var identity = GetCurrentIdentity(); if (identity is null) return null; var batch = await sessionStore.GetBatchAsync(batchId); if (batch is null) return null; return await sessionStore.IsGroupManagerAsync(batch.GroupId, identity.Value.Platform, identity.Value.ExternalUserId) ? batch : null; } public async Task UpdateSessionForCurrentUserAsync(Guid sessionId, string title, DateTime scheduledAt, string joinLink, int? maxPlayers) { var identity = GetCurrentIdentity(); if (identity is null) throw new InvalidOperationException("User is not authenticated."); var session = await GetSessionForCurrentUserAsync(sessionId); if (session is null) { throw new SessionAccessDeniedException(sessionId, identity.Value.ExternalUserId); } await sessionStore.UpdateSessionAsync(sessionId, session.GroupId, title, scheduledAt, joinLink, maxPlayers); if (session.Title != title) await sessionStore.LogSessionChangeAsync(sessionId, identity.Value.ExternalUserId, identity.Value.Name, "Title", session.Title, title); if (session.ScheduledAt != scheduledAt) await sessionStore.LogSessionChangeAsync(sessionId, identity.Value.ExternalUserId, identity.Value.Name, "Time", session.ScheduledAt.ToString("O"), scheduledAt.ToString("O")); if (session.JoinLink != joinLink) await sessionStore.LogSessionChangeAsync(sessionId, identity.Value.ExternalUserId, identity.Value.Name, "Link", session.JoinLink, joinLink); if (session.MaxPlayers != maxPlayers) await sessionStore.LogSessionChangeAsync(sessionId, identity.Value.ExternalUserId, identity.Value.Name, "MaxPlayers", session.MaxPlayers?.ToString(), maxPlayers?.ToString()); } public async Task PromoteWaitlistedPlayerForCurrentUserAsync(Guid sessionId) { var identity = GetCurrentIdentity(); if (identity is null) throw new InvalidOperationException("User is not authenticated."); var session = await GetSessionForCurrentUserAsync(sessionId); if (session is null) { throw new SessionAccessDeniedException(sessionId, identity.Value.ExternalUserId); } await sessionStore.PromoteWaitlistedPlayerAsync(sessionId, session.GroupId); await sessionStore.LogSessionChangeAsync(sessionId, identity.Value.ExternalUserId, identity.Value.Name, "WaitlistPromote", null, null); } public async Task UpdateBatchDetailsForCurrentUserAsync(Guid batchId, string title, string joinLink) { var identity = GetCurrentIdentity(); if (identity is null) throw new InvalidOperationException("User is not authenticated."); var batch = await GetBatchForCurrentUserAsync(batchId); if (batch is null) { throw new SessionAccessDeniedException(batchId, identity.Value.ExternalUserId); } await sessionStore.UpdateBatchDetailsAsync(batchId, batch.GroupId, title.Trim(), joinLink.Trim()); } public async Task UpdateBatchNotificationModeForCurrentUserAsync(Guid batchId, SessionNotificationMode notificationMode) { var identity = GetCurrentIdentity(); if (identity is null) throw new InvalidOperationException("User is not authenticated."); var batch = await GetBatchForCurrentUserAsync(batchId); if (batch is null) { throw new SessionAccessDeniedException(batchId, identity.Value.ExternalUserId); } await sessionStore.UpdateBatchNotificationModeAsync(batchId, batch.GroupId, notificationMode); } public async Task RescheduleBatchForCurrentUserAsync(Guid batchId, DateTime firstScheduledAt, int intervalDays) { if (intervalDays <= 0) { throw new ArgumentOutOfRangeException(nameof(intervalDays), intervalDays, "Interval must be greater than zero."); } var identity = GetCurrentIdentity(); if (identity is null) throw new InvalidOperationException("User is not authenticated."); var batch = await GetBatchForCurrentUserAsync(batchId); if (batch is null) { throw new SessionAccessDeniedException(batchId, identity.Value.ExternalUserId); } await sessionStore.RescheduleBatchAsync(batchId, batch.GroupId, firstScheduledAt, intervalDays); await sessionStore.LogSessionChangeAsync(batchId, identity.Value.ExternalUserId, identity.Value.Name, "BatchRescheduled", batch.FirstScheduledAt.ToString("O"), firstScheduledAt.ToString("O")); } public async Task CloneBatchForCurrentUserAsync(Guid batchId, BatchCloneInterval interval) { var identity = GetCurrentIdentity(); if (identity is null) throw new InvalidOperationException("User is not authenticated."); var batch = await GetBatchForCurrentUserAsync(batchId); if (batch is null) { throw new SessionAccessDeniedException(batchId, identity.Value.ExternalUserId); } return await sessionStore.CloneBatchAsync(batchId, batch.GroupId, interval); } public async Task?> GetCampaignTemplatesForCurrentUserAsync(Guid groupId) { var identity = GetCurrentIdentity(); if (identity is null) return null; if (!await sessionStore.IsGroupManagerAsync(groupId, identity.Value.Platform, identity.Value.ExternalUserId)) return null; return await sessionStore.GetCampaignTemplatesAsync(groupId); } public async Task CreateCampaignTemplateForCurrentUserAsync( Guid groupId, CreateCampaignTemplateRequest request) { var identity = GetCurrentIdentity(); if (identity is null) throw new InvalidOperationException("User is not authenticated."); if (!await sessionStore.IsGroupManagerAsync(groupId, identity.Value.Platform, identity.Value.ExternalUserId)) { throw new SessionAccessDeniedException(groupId, identity.Value.ExternalUserId); } var normalizedRequest = NormalizeCampaignTemplateRequest(request); return await sessionStore.CreateCampaignTemplateAsync(groupId, normalizedRequest); } public async Task DeleteCampaignTemplateForCurrentUserAsync(Guid templateId) { var identity = GetCurrentIdentity(); if (identity is null) throw new InvalidOperationException("User is not authenticated."); var template = await sessionStore.GetCampaignTemplateAsync(templateId); if (template is null || !await sessionStore.IsGroupManagerAsync(template.GroupId, identity.Value.Platform, identity.Value.ExternalUserId)) { throw new SessionAccessDeniedException(templateId, identity.Value.ExternalUserId); } await sessionStore.DeleteCampaignTemplateAsync(templateId, template.GroupId); } public async Task CreateBatchFromCampaignTemplateForCurrentUserAsync( Guid templateId, DateTime firstScheduledAt) { if (firstScheduledAt <= DateTime.UtcNow) { throw new ArgumentOutOfRangeException(nameof(firstScheduledAt), firstScheduledAt, "First scheduled time must be in the future."); } var identity = GetCurrentIdentity(); if (identity is null) throw new InvalidOperationException("User is not authenticated."); var template = await sessionStore.GetCampaignTemplateAsync(templateId); if (template is null || !await sessionStore.IsGroupManagerAsync(template.GroupId, identity.Value.Platform, identity.Value.ExternalUserId)) { throw new SessionAccessDeniedException(templateId, identity.Value.ExternalUserId); } return await sessionStore.CreateBatchFromTemplateAsync(templateId, template.GroupId, firstScheduledAt); } public async Task AddCoGmForOwnerAsync(Guid groupId, string coGmPlatform, string coGmExternalUserId, string displayName, string? externalUsername) { if (string.IsNullOrWhiteSpace(coGmExternalUserId)) { throw new ArgumentException("External user id must not be empty.", nameof(coGmExternalUserId)); } var identity = GetCurrentIdentity(); if (identity is null) throw new InvalidOperationException("User is not authenticated."); if (identity.Value.ExternalUserId == coGmExternalUserId && identity.Value.Platform == coGmPlatform) { throw new InvalidOperationException("Owner is already a group manager."); } if (!await sessionStore.IsGroupOwnerAsync(groupId, identity.Value.Platform, identity.Value.ExternalUserId)) { throw new SessionAccessDeniedException(groupId, identity.Value.ExternalUserId); } var normalizedName = string.IsNullOrWhiteSpace(displayName) ? $"{coGmPlatform} {coGmExternalUserId}" : displayName.Trim(); var normalizedUsername = string.IsNullOrWhiteSpace(externalUsername) ? null : externalUsername.Trim().TrimStart('@'); await sessionStore.AddGroupCoGmAsync( groupId, identity.Value.Platform, identity.Value.ExternalUserId, coGmPlatform, coGmExternalUserId, normalizedName, normalizedUsername); } public async Task RemoveCoGmForOwnerAsync(Guid groupId, string coGmPlatform, string coGmExternalUserId) { var identity = GetCurrentIdentity(); if (identity is null) throw new InvalidOperationException("User is not authenticated."); if (!await sessionStore.IsGroupOwnerAsync(groupId, identity.Value.Platform, identity.Value.ExternalUserId)) { throw new SessionAccessDeniedException(groupId, identity.Value.ExternalUserId); } await sessionStore.RemoveGroupCoGmAsync(groupId, coGmPlatform, coGmExternalUserId); } public async Task?> GetSessionParticipantsForCurrentUserAsync(Guid sessionId) { var session = await GetSessionForCurrentUserAsync(sessionId); if (session is null) return null; return await sessionStore.GetSessionParticipantsAsync(sessionId); } public async Task?> GetSessionHistoryForCurrentUserAsync(Guid sessionId) { var session = await GetSessionForCurrentUserAsync(sessionId); if (session is null) return null; return await sessionStore.GetSessionHistoryAsync(sessionId); } public async Task RemovePlayerFromSessionForCurrentUserAsync(Guid sessionId, Guid participantId) { var identity = GetCurrentIdentity(); if (identity is null) throw new InvalidOperationException("User is not authenticated."); var session = await GetSessionForCurrentUserAsync(sessionId); if (session is null) { throw new SessionAccessDeniedException(sessionId, identity.Value.ExternalUserId); } await sessionStore.RemovePlayerFromSessionAsync(sessionId, session.GroupId, participantId); await sessionStore.LogSessionChangeAsync(sessionId, identity.Value.ExternalUserId, identity.Value.Name, "PlayerRemoved", participantId.ToString(), null); } public async Task?> GetGroupAttendanceStatsForCurrentUserAsync(Guid groupId) { var identity = GetCurrentIdentity(); if (identity is null) return null; if (!await sessionStore.IsGroupManagerAsync(groupId, identity.Value.Platform, identity.Value.ExternalUserId)) return null; return await sessionStore.GetGroupAttendanceStatsAsync(groupId); } private static CreateCampaignTemplateRequest NormalizeCampaignTemplateRequest(CreateCampaignTemplateRequest request) { var name = request.Name.Trim(); var title = request.Title.Trim(); var joinLink = request.JoinLink.Trim(); if (name.Length == 0) { throw new ArgumentException("Template name must not be empty.", nameof(request)); } if (title.Length == 0) { throw new ArgumentException("Session title must not be empty.", nameof(request)); } if (joinLink.Length == 0) { throw new ArgumentException("Join link must not be empty.", nameof(request)); } if (request.SessionCount is < 1 or > 52) { throw new ArgumentOutOfRangeException(nameof(request), request.SessionCount, "Session count must be between 1 and 52."); } if (request.IntervalDays is < 1 or > 365) { throw new ArgumentOutOfRangeException(nameof(request), request.IntervalDays, "Interval must be between 1 and 365 days."); } if (request.MaxPlayers is <= 0) { throw new ArgumentOutOfRangeException(nameof(request), request.MaxPlayers, "Seat limit must be greater than zero."); } return request with { Name = name, Title = title, JoinLink = joinLink }; } }