9d4256353d
- ISessionStore: all methods use (platform, external_user_id) - SessionService: updated SQL queries and added UpsertDiscordUserAsync - AuthorizedSessionService: resolves identity from HttpContext, no longer accepts telegram_id params - SessionAccessDeniedException now accepts string externalUserId - Added ExternalUserId/ExternalUsername to WebGroupManager and WebParticipant Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
394 lines
16 KiB
C#
394 lines
16 KiB
C#
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<List<WebGameGroup>> GetGroupsForCurrentUserAsync()
|
|
{
|
|
var identity = GetCurrentIdentity();
|
|
if (identity is null)
|
|
return Task.FromResult(new List<WebGameGroup>());
|
|
|
|
return sessionStore.GetGroupsForUserAsync(identity.Value.Platform, identity.Value.ExternalUserId);
|
|
}
|
|
|
|
public async Task<WebGroupManagement?> 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<List<WebSession>?> 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<WebSession?> 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<WebSessionBatch?> 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<WebSessionBatch> 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<List<WebCampaignTemplate>?> 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<WebCampaignTemplate> 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<WebSessionBatch> 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<List<WebParticipant>?> GetSessionParticipantsForCurrentUserAsync(Guid sessionId)
|
|
{
|
|
var session = await GetSessionForCurrentUserAsync(sessionId);
|
|
if (session is null)
|
|
return null;
|
|
|
|
return await sessionStore.GetSessionParticipantsAsync(sessionId);
|
|
}
|
|
|
|
public async Task<List<SessionAuditLogEntry>?> 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<List<PlayerAttendanceStats>?> 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
|
|
};
|
|
}
|
|
}
|