feat(web): refactor SessionStore and AuthorizedSessionService to platform-agnostic identity
- 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>
This commit is contained in:
@@ -1,182 +1,237 @@
|
||||
using System.Security.Claims;
|
||||
using GmRelay.Shared.Domain;
|
||||
|
||||
namespace GmRelay.Web.Services;
|
||||
|
||||
public sealed class AuthorizedSessionService(ISessionStore sessionStore)
|
||||
public sealed class AuthorizedSessionService(ISessionStore sessionStore, IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
public Task<List<WebGameGroup>> GetGroupsForGmAsync(long gmId) =>
|
||||
sessionStore.GetGroupsForGmAsync(gmId);
|
||||
|
||||
public async Task<WebGroupManagement?> GetGroupManagementForGmAsync(Guid groupId, long gmId)
|
||||
private (string Platform, string ExternalUserId, string Name)? GetCurrentIdentity()
|
||||
{
|
||||
if (!await GroupBelongsToGmAsync(groupId, gmId))
|
||||
{
|
||||
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, gmId);
|
||||
var isOwner = await sessionStore.IsGroupOwnerAsync(groupId, identity.Value.Platform, identity.Value.ExternalUserId);
|
||||
return new WebGroupManagement(group, managers, isOwner);
|
||||
}
|
||||
|
||||
public async Task<List<WebSession>?> GetUpcomingSessionsForGmAsync(Guid groupId, long gmId)
|
||||
public async Task<List<WebSession>?> GetUpcomingSessionsForCurrentUserAsync(Guid groupId)
|
||||
{
|
||||
if (!await GroupBelongsToGmAsync(groupId, gmId))
|
||||
{
|
||||
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?> GetSessionForGmAsync(Guid sessionId, long gmId)
|
||||
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 GroupBelongsToGmAsync(session.GroupId, gmId) ? session : null;
|
||||
return await sessionStore.IsGroupManagerAsync(session.GroupId, identity.Value.Platform, identity.Value.ExternalUserId) ? session : null;
|
||||
}
|
||||
|
||||
public async Task<WebSessionBatch?> GetBatchForGmAsync(Guid batchId, long gmId)
|
||||
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 GroupBelongsToGmAsync(batch.GroupId, gmId) ? batch : null;
|
||||
return await sessionStore.IsGroupManagerAsync(batch.GroupId, identity.Value.Platform, identity.Value.ExternalUserId) ? batch : null;
|
||||
}
|
||||
|
||||
public async Task UpdateSessionForGmAsync(Guid sessionId, long gmId, string title, DateTime scheduledAt, string joinLink, int? maxPlayers)
|
||||
public async Task UpdateSessionForCurrentUserAsync(Guid sessionId, string title, DateTime scheduledAt, string joinLink, int? maxPlayers)
|
||||
{
|
||||
var session = await GetSessionForGmAsync(sessionId, gmId);
|
||||
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, gmId);
|
||||
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, gmId, "ГМ", "Title", 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, gmId, "ГМ", "Time", session.ScheduledAt.ToString("O"), scheduledAt.ToString("O"));
|
||||
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, gmId, "ГМ", "Link", 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, gmId, "ГМ", "MaxPlayers", session.MaxPlayers?.ToString(), maxPlayers?.ToString());
|
||||
await sessionStore.LogSessionChangeAsync(sessionId, identity.Value.ExternalUserId, identity.Value.Name, "MaxPlayers", session.MaxPlayers?.ToString(), maxPlayers?.ToString());
|
||||
}
|
||||
|
||||
public async Task PromoteWaitlistedPlayerForGmAsync(Guid sessionId, long gmId)
|
||||
public async Task PromoteWaitlistedPlayerForCurrentUserAsync(Guid sessionId)
|
||||
{
|
||||
var session = await GetSessionForGmAsync(sessionId, gmId);
|
||||
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, gmId);
|
||||
throw new SessionAccessDeniedException(sessionId, identity.Value.ExternalUserId);
|
||||
}
|
||||
|
||||
await sessionStore.PromoteWaitlistedPlayerAsync(sessionId, session.GroupId);
|
||||
await sessionStore.LogSessionChangeAsync(sessionId, gmId, "ГМ", "WaitlistPromote", null, null);
|
||||
await sessionStore.LogSessionChangeAsync(sessionId, identity.Value.ExternalUserId, identity.Value.Name, "WaitlistPromote", null, null);
|
||||
}
|
||||
|
||||
public async Task UpdateBatchDetailsForGmAsync(Guid batchId, long gmId, string title, string joinLink)
|
||||
public async Task UpdateBatchDetailsForCurrentUserAsync(Guid batchId, string title, string joinLink)
|
||||
{
|
||||
var batch = await GetBatchForGmAsync(batchId, gmId);
|
||||
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, gmId);
|
||||
throw new SessionAccessDeniedException(batchId, identity.Value.ExternalUserId);
|
||||
}
|
||||
|
||||
await sessionStore.UpdateBatchDetailsAsync(batchId, batch.GroupId, title.Trim(), joinLink.Trim());
|
||||
}
|
||||
|
||||
public async Task UpdateBatchNotificationModeForGmAsync(Guid batchId, long gmId, SessionNotificationMode notificationMode)
|
||||
public async Task UpdateBatchNotificationModeForCurrentUserAsync(Guid batchId, SessionNotificationMode notificationMode)
|
||||
{
|
||||
var batch = await GetBatchForGmAsync(batchId, gmId);
|
||||
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, gmId);
|
||||
throw new SessionAccessDeniedException(batchId, identity.Value.ExternalUserId);
|
||||
}
|
||||
|
||||
await sessionStore.UpdateBatchNotificationModeAsync(batchId, batch.GroupId, notificationMode);
|
||||
}
|
||||
|
||||
public async Task RescheduleBatchForGmAsync(Guid batchId, long gmId, DateTime firstScheduledAt, int intervalDays)
|
||||
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 batch = await GetBatchForGmAsync(batchId, gmId);
|
||||
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, gmId);
|
||||
throw new SessionAccessDeniedException(batchId, identity.Value.ExternalUserId);
|
||||
}
|
||||
|
||||
await sessionStore.RescheduleBatchAsync(batchId, batch.GroupId, firstScheduledAt, intervalDays);
|
||||
await sessionStore.LogSessionChangeAsync(batchId, gmId, "ГМ", "BatchRescheduled", batch.FirstScheduledAt.ToString("O"), firstScheduledAt.ToString("O"));
|
||||
await sessionStore.LogSessionChangeAsync(batchId, identity.Value.ExternalUserId, identity.Value.Name, "BatchRescheduled", batch.FirstScheduledAt.ToString("O"), firstScheduledAt.ToString("O"));
|
||||
}
|
||||
|
||||
public async Task<WebSessionBatch> CloneBatchForGmAsync(Guid batchId, long gmId, BatchCloneInterval interval)
|
||||
public async Task<WebSessionBatch> CloneBatchForCurrentUserAsync(Guid batchId, BatchCloneInterval interval)
|
||||
{
|
||||
var batch = await GetBatchForGmAsync(batchId, gmId);
|
||||
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, gmId);
|
||||
throw new SessionAccessDeniedException(batchId, identity.Value.ExternalUserId);
|
||||
}
|
||||
|
||||
return await sessionStore.CloneBatchAsync(batchId, batch.GroupId, interval);
|
||||
}
|
||||
|
||||
public async Task<List<WebCampaignTemplate>?> GetCampaignTemplatesForGmAsync(Guid groupId, long gmId)
|
||||
public async Task<List<WebCampaignTemplate>?> GetCampaignTemplatesForCurrentUserAsync(Guid groupId)
|
||||
{
|
||||
if (!await GroupBelongsToGmAsync(groupId, gmId))
|
||||
{
|
||||
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> CreateCampaignTemplateForGmAsync(
|
||||
public async Task<WebCampaignTemplate> CreateCampaignTemplateForCurrentUserAsync(
|
||||
Guid groupId,
|
||||
long gmId,
|
||||
CreateCampaignTemplateRequest request)
|
||||
{
|
||||
if (!await GroupBelongsToGmAsync(groupId, gmId))
|
||||
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, gmId);
|
||||
throw new SessionAccessDeniedException(groupId, identity.Value.ExternalUserId);
|
||||
}
|
||||
|
||||
var normalizedRequest = NormalizeCampaignTemplateRequest(request);
|
||||
return await sessionStore.CreateCampaignTemplateAsync(groupId, normalizedRequest);
|
||||
}
|
||||
|
||||
public async Task DeleteCampaignTemplateForGmAsync(Guid templateId, long gmId)
|
||||
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 GroupBelongsToGmAsync(template.GroupId, gmId))
|
||||
if (template is null || !await sessionStore.IsGroupManagerAsync(template.GroupId, identity.Value.Platform, identity.Value.ExternalUserId))
|
||||
{
|
||||
throw new SessionAccessDeniedException(templateId, gmId);
|
||||
throw new SessionAccessDeniedException(templateId, identity.Value.ExternalUserId);
|
||||
}
|
||||
|
||||
await sessionStore.DeleteCampaignTemplateAsync(templateId, template.GroupId);
|
||||
}
|
||||
|
||||
public async Task<WebSessionBatch> CreateBatchFromCampaignTemplateForGmAsync(
|
||||
public async Task<WebSessionBatch> CreateBatchFromCampaignTemplateForCurrentUserAsync(
|
||||
Guid templateId,
|
||||
long gmId,
|
||||
DateTime firstScheduledAt)
|
||||
{
|
||||
if (firstScheduledAt <= DateTime.UtcNow)
|
||||
@@ -184,89 +239,112 @@ public sealed class AuthorizedSessionService(ISessionStore sessionStore)
|
||||
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 GroupBelongsToGmAsync(template.GroupId, gmId))
|
||||
if (template is null || !await sessionStore.IsGroupManagerAsync(template.GroupId, identity.Value.Platform, identity.Value.ExternalUserId))
|
||||
{
|
||||
throw new SessionAccessDeniedException(templateId, gmId);
|
||||
throw new SessionAccessDeniedException(templateId, identity.Value.ExternalUserId);
|
||||
}
|
||||
|
||||
return await sessionStore.CreateBatchFromTemplateAsync(templateId, template.GroupId, firstScheduledAt);
|
||||
}
|
||||
|
||||
public async Task AddCoGmForOwnerAsync(Guid groupId, long ownerTelegramId, long coGmTelegramId, string displayName, string? telegramUsername)
|
||||
public async Task AddCoGmForOwnerAsync(Guid groupId, string coGmPlatform, string coGmExternalUserId, string displayName, string? externalUsername)
|
||||
{
|
||||
if (coGmTelegramId <= 0)
|
||||
if (string.IsNullOrWhiteSpace(coGmExternalUserId))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(coGmTelegramId), coGmTelegramId, "Telegram id must be greater than zero.");
|
||||
throw new ArgumentException("External user id must not be empty.", nameof(coGmExternalUserId));
|
||||
}
|
||||
|
||||
if (ownerTelegramId == coGmTelegramId)
|
||||
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, ownerTelegramId))
|
||||
if (!await sessionStore.IsGroupOwnerAsync(groupId, identity.Value.Platform, identity.Value.ExternalUserId))
|
||||
{
|
||||
throw new SessionAccessDeniedException(groupId, ownerTelegramId);
|
||||
throw new SessionAccessDeniedException(groupId, identity.Value.ExternalUserId);
|
||||
}
|
||||
|
||||
var normalizedName = string.IsNullOrWhiteSpace(displayName)
|
||||
? $"Telegram {coGmTelegramId.ToString(System.Globalization.CultureInfo.InvariantCulture)}"
|
||||
? $"{coGmPlatform} {coGmExternalUserId}"
|
||||
: displayName.Trim();
|
||||
var normalizedUsername = string.IsNullOrWhiteSpace(telegramUsername)
|
||||
var normalizedUsername = string.IsNullOrWhiteSpace(externalUsername)
|
||||
? null
|
||||
: telegramUsername.Trim().TrimStart('@');
|
||||
: externalUsername.Trim().TrimStart('@');
|
||||
|
||||
await sessionStore.AddGroupCoGmAsync(groupId, ownerTelegramId, coGmTelegramId, normalizedName, normalizedUsername);
|
||||
await sessionStore.AddGroupCoGmAsync(
|
||||
groupId,
|
||||
identity.Value.Platform, identity.Value.ExternalUserId,
|
||||
coGmPlatform, coGmExternalUserId,
|
||||
normalizedName, normalizedUsername);
|
||||
}
|
||||
|
||||
public async Task RemoveCoGmForOwnerAsync(Guid groupId, long ownerTelegramId, long coGmTelegramId)
|
||||
public async Task RemoveCoGmForOwnerAsync(Guid groupId, string coGmPlatform, string coGmExternalUserId)
|
||||
{
|
||||
if (!await sessionStore.IsGroupOwnerAsync(groupId, ownerTelegramId))
|
||||
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, ownerTelegramId);
|
||||
throw new SessionAccessDeniedException(groupId, identity.Value.ExternalUserId);
|
||||
}
|
||||
|
||||
await sessionStore.RemoveGroupCoGmAsync(groupId, coGmTelegramId);
|
||||
await sessionStore.RemoveGroupCoGmAsync(groupId, coGmPlatform, coGmExternalUserId);
|
||||
}
|
||||
|
||||
public async Task<List<WebParticipant>?> GetSessionParticipantsForGmAsync(Guid sessionId, long gmId)
|
||||
public async Task<List<WebParticipant>?> GetSessionParticipantsForCurrentUserAsync(Guid sessionId)
|
||||
{
|
||||
var session = await GetSessionForGmAsync(sessionId, gmId);
|
||||
var session = await GetSessionForCurrentUserAsync(sessionId);
|
||||
if (session is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await sessionStore.GetSessionParticipantsAsync(sessionId);
|
||||
}
|
||||
|
||||
public async Task<List<SessionAuditLogEntry>?> GetSessionHistoryForGmAsync(Guid sessionId, long gmId)
|
||||
public async Task<List<SessionAuditLogEntry>?> GetSessionHistoryForCurrentUserAsync(Guid sessionId)
|
||||
{
|
||||
var session = await GetSessionForGmAsync(sessionId, gmId);
|
||||
var session = await GetSessionForCurrentUserAsync(sessionId);
|
||||
if (session is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await sessionStore.GetSessionHistoryAsync(sessionId);
|
||||
}
|
||||
|
||||
public async Task RemovePlayerFromSessionForGmAsync(Guid sessionId, long gmId, Guid participantId)
|
||||
public async Task RemovePlayerFromSessionForCurrentUserAsync(Guid sessionId, Guid participantId)
|
||||
{
|
||||
var session = await GetSessionForGmAsync(sessionId, gmId);
|
||||
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, gmId);
|
||||
throw new SessionAccessDeniedException(sessionId, identity.Value.ExternalUserId);
|
||||
}
|
||||
|
||||
await sessionStore.RemovePlayerFromSessionAsync(sessionId, session.GroupId, participantId);
|
||||
await sessionStore.LogSessionChangeAsync(sessionId, gmId, "ГМ", "PlayerRemoved", participantId.ToString(), null);
|
||||
await sessionStore.LogSessionChangeAsync(sessionId, identity.Value.ExternalUserId, identity.Value.Name, "PlayerRemoved", participantId.ToString(), null);
|
||||
}
|
||||
|
||||
private async Task<bool> GroupBelongsToGmAsync(Guid groupId, long gmId)
|
||||
public async Task<List<PlayerAttendanceStats>?> GetGroupAttendanceStatsForCurrentUserAsync(Guid groupId)
|
||||
{
|
||||
return await sessionStore.IsGroupManagerAsync(groupId, gmId);
|
||||
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)
|
||||
|
||||
@@ -5,33 +5,31 @@ namespace GmRelay.Web.Services;
|
||||
public sealed record PlayerAttendanceStats(
|
||||
Guid PlayerId,
|
||||
string DisplayName,
|
||||
string? TelegramUsername,
|
||||
string? ExternalUsername,
|
||||
long TotalSessions,
|
||||
long ConfirmedCount,
|
||||
long DeclinedCount,
|
||||
long NoResponseCount,
|
||||
long WaitlistedCount,
|
||||
long CancellationAffectedCount,
|
||||
decimal AttendanceRate
|
||||
);
|
||||
decimal AttendanceRate);
|
||||
|
||||
public sealed record SessionAuditLogEntry(
|
||||
Guid Id,
|
||||
Guid SessionId,
|
||||
long ActorTelegramId,
|
||||
string ActorExternalUserId,
|
||||
string ActorName,
|
||||
string ChangeType,
|
||||
string? OldValue,
|
||||
string? NewValue,
|
||||
DateTime ChangedAt
|
||||
);
|
||||
DateTime ChangedAt);
|
||||
|
||||
public interface ISessionStore
|
||||
{
|
||||
Task<List<WebGameGroup>> GetGroupsForGmAsync(long gmId);
|
||||
Task<List<WebGameGroup>> GetGroupsForUserAsync(string platform, string externalUserId);
|
||||
Task<WebGameGroup?> GetGroupAsync(Guid groupId);
|
||||
Task<bool> IsGroupManagerAsync(Guid groupId, long telegramId);
|
||||
Task<bool> IsGroupOwnerAsync(Guid groupId, long telegramId);
|
||||
Task<bool> IsGroupManagerAsync(Guid groupId, string platform, string externalUserId);
|
||||
Task<bool> IsGroupOwnerAsync(Guid groupId, string platform, string externalUserId);
|
||||
Task<List<WebGroupManager>> GetGroupManagersAsync(Guid groupId);
|
||||
Task<List<WebSession>> GetUpcomingSessionsAsync(Guid groupId);
|
||||
Task<WebSession?> GetSessionAsync(Guid sessionId);
|
||||
@@ -47,11 +45,12 @@ public interface ISessionStore
|
||||
Task<WebCampaignTemplate> CreateCampaignTemplateAsync(Guid groupId, CreateCampaignTemplateRequest request);
|
||||
Task DeleteCampaignTemplateAsync(Guid templateId, Guid groupId);
|
||||
Task<WebSessionBatch> CreateBatchFromTemplateAsync(Guid templateId, Guid groupId, DateTime firstScheduledAt);
|
||||
Task AddGroupCoGmAsync(Guid groupId, long ownerTelegramId, long coGmTelegramId, string displayName, string? telegramUsername);
|
||||
Task RemoveGroupCoGmAsync(Guid groupId, long coGmTelegramId);
|
||||
Task AddGroupCoGmAsync(Guid groupId, string ownerPlatform, string ownerExternalUserId, string coGmPlatform, string coGmExternalUserId, string displayName, string? externalUsername);
|
||||
Task RemoveGroupCoGmAsync(Guid groupId, string coGmPlatform, string coGmExternalUserId);
|
||||
Task<List<WebParticipant>> GetSessionParticipantsAsync(Guid sessionId);
|
||||
Task RemovePlayerFromSessionAsync(Guid sessionId, Guid groupId, Guid participantId);
|
||||
Task<List<PlayerAttendanceStats>> GetGroupAttendanceStatsAsync(Guid groupId);
|
||||
Task LogSessionChangeAsync(Guid sessionId, long actorTelegramId, string actorName, string changeType, string? oldValue, string? newValue);
|
||||
Task LogSessionChangeAsync(Guid sessionId, string actorExternalUserId, string actorName, string changeType, string? oldValue, string? newValue);
|
||||
Task<List<SessionAuditLogEntry>> GetSessionHistoryAsync(Guid sessionId);
|
||||
Task UpsertDiscordUserAsync(string discordId, string displayName, string? avatarUrl);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace GmRelay.Web.Services;
|
||||
|
||||
public sealed class SessionAccessDeniedException(Guid sessionId, long gmId)
|
||||
: InvalidOperationException($"Session '{sessionId}' is not accessible for GM '{gmId}'.");
|
||||
public sealed class SessionAccessDeniedException(Guid sessionId, string externalUserId)
|
||||
: InvalidOperationException($"Session '{sessionId}' is not accessible for user '{externalUserId}'.");
|
||||
|
||||
@@ -10,14 +10,17 @@ namespace GmRelay.Web.Services;
|
||||
public sealed record WebGameGroup(
|
||||
Guid Id,
|
||||
long TelegramChatId,
|
||||
string? ExternalGroupId,
|
||||
string Name,
|
||||
long GmTelegramId,
|
||||
string? Platform,
|
||||
string ManagerRole = GroupManagerRoleExtensions.OwnerValue);
|
||||
|
||||
public sealed record WebGroupManager(
|
||||
long TelegramId,
|
||||
string? ExternalUserId,
|
||||
string DisplayName,
|
||||
string? TelegramUsername,
|
||||
string? ExternalUsername,
|
||||
string Role,
|
||||
DateTime AddedAt);
|
||||
|
||||
@@ -44,8 +47,10 @@ public sealed record WebSession(
|
||||
public sealed record WebParticipant(
|
||||
Guid Id,
|
||||
long TelegramId,
|
||||
string? ExternalUserId,
|
||||
string DisplayName,
|
||||
string? TelegramUsername,
|
||||
string? ExternalUsername,
|
||||
string RsvpStatus,
|
||||
string RegistrationStatus,
|
||||
bool IsGm,
|
||||
@@ -83,23 +88,25 @@ public sealed class SessionService(
|
||||
ITelegramBotClient bot,
|
||||
ILogger<SessionService> logger) : ISessionStore
|
||||
{
|
||||
public async Task<List<WebGameGroup>> GetGroupsForGmAsync(long gmId)
|
||||
public async Task<List<WebGameGroup>> GetGroupsForUserAsync(string platform, string externalUserId)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
return (await conn.QueryAsync<WebGameGroup>(
|
||||
"""
|
||||
SELECT g.id,
|
||||
g.telegram_chat_id AS TelegramChatId,
|
||||
g.external_group_id AS ExternalGroupId,
|
||||
g.name,
|
||||
g.gm_telegram_id AS GmTelegramId,
|
||||
g.platform AS Platform,
|
||||
gm.role AS ManagerRole
|
||||
FROM group_managers gm
|
||||
JOIN players p ON p.id = gm.player_id
|
||||
JOIN game_groups g ON g.id = gm.group_id
|
||||
WHERE p.telegram_id = @GmId
|
||||
WHERE p.platform = @Platform
|
||||
AND p.external_user_id = @ExternalUserId
|
||||
ORDER BY g.name
|
||||
""",
|
||||
new { GmId = gmId })).ToList();
|
||||
new { Platform = platform, ExternalUserId = externalUserId })).ToList();
|
||||
}
|
||||
|
||||
public async Task<WebGameGroup?> GetGroupAsync(Guid groupId)
|
||||
@@ -109,8 +116,9 @@ public sealed class SessionService(
|
||||
"""
|
||||
SELECT g.id,
|
||||
g.telegram_chat_id AS TelegramChatId,
|
||||
g.external_group_id AS ExternalGroupId,
|
||||
g.name,
|
||||
g.gm_telegram_id AS GmTelegramId,
|
||||
g.platform AS Platform,
|
||||
@OwnerRole AS ManagerRole
|
||||
FROM game_groups g
|
||||
WHERE g.id = @GroupId
|
||||
@@ -118,7 +126,7 @@ public sealed class SessionService(
|
||||
new { GroupId = groupId, OwnerRole = GroupManagerRoleExtensions.OwnerValue });
|
||||
}
|
||||
|
||||
public async Task<bool> IsGroupManagerAsync(Guid groupId, long telegramId)
|
||||
public async Task<bool> IsGroupManagerAsync(Guid groupId, string platform, string externalUserId)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
return await conn.ExecuteScalarAsync<bool>(
|
||||
@@ -128,13 +136,14 @@ public sealed class SessionService(
|
||||
FROM group_managers gm
|
||||
JOIN players p ON p.id = gm.player_id
|
||||
WHERE gm.group_id = @GroupId
|
||||
AND p.telegram_id = @TelegramId
|
||||
AND p.platform = @Platform
|
||||
AND p.external_user_id = @ExternalUserId
|
||||
)
|
||||
""",
|
||||
new { GroupId = groupId, TelegramId = telegramId });
|
||||
new { GroupId = groupId, Platform = platform, ExternalUserId = externalUserId });
|
||||
}
|
||||
|
||||
public async Task<bool> IsGroupOwnerAsync(Guid groupId, long telegramId)
|
||||
public async Task<bool> IsGroupOwnerAsync(Guid groupId, string platform, string externalUserId)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
return await conn.ExecuteScalarAsync<bool>(
|
||||
@@ -144,11 +153,12 @@ public sealed class SessionService(
|
||||
FROM group_managers gm
|
||||
JOIN players p ON p.id = gm.player_id
|
||||
WHERE gm.group_id = @GroupId
|
||||
AND p.telegram_id = @TelegramId
|
||||
AND p.platform = @Platform
|
||||
AND p.external_user_id = @ExternalUserId
|
||||
AND gm.role = @OwnerRole
|
||||
)
|
||||
""",
|
||||
new { GroupId = groupId, TelegramId = telegramId, OwnerRole = GroupManagerRoleExtensions.OwnerValue });
|
||||
new { GroupId = groupId, Platform = platform, ExternalUserId = externalUserId, OwnerRole = GroupManagerRoleExtensions.OwnerValue });
|
||||
}
|
||||
|
||||
public async Task<List<WebGroupManager>> GetGroupManagersAsync(Guid groupId)
|
||||
@@ -157,8 +167,10 @@ public sealed class SessionService(
|
||||
return (await conn.QueryAsync<WebGroupManager>(
|
||||
"""
|
||||
SELECT p.telegram_id AS TelegramId,
|
||||
COALESCE(p.external_user_id, p.telegram_id::TEXT) AS ExternalUserId,
|
||||
p.display_name AS DisplayName,
|
||||
p.telegram_username AS TelegramUsername,
|
||||
COALESCE(p.external_username, p.telegram_username) AS ExternalUsername,
|
||||
gm.role AS Role,
|
||||
gm.created_at AS AddedAt
|
||||
FROM group_managers gm
|
||||
@@ -179,7 +191,7 @@ public sealed class SessionService(
|
||||
SELECT
|
||||
p.id AS PlayerId,
|
||||
p.display_name AS DisplayName,
|
||||
p.telegram_username AS TelegramUsername,
|
||||
COALESCE(p.external_username, p.telegram_username) AS ExternalUsername,
|
||||
COUNT(DISTINCT s.id) AS TotalSessions,
|
||||
COUNT(DISTINCT CASE WHEN sp.rsvp_status = 'Confirmed' THEN s.id END) AS ConfirmedCount,
|
||||
COUNT(DISTINCT CASE WHEN sp.rsvp_status = 'Declined' THEN s.id END) AS DeclinedCount,
|
||||
@@ -198,21 +210,21 @@ public sealed class SessionService(
|
||||
WHERE s.group_id = @GroupId
|
||||
AND s.scheduled_at <= now()
|
||||
AND sp.is_gm = false
|
||||
GROUP BY p.id, p.display_name, p.telegram_username
|
||||
GROUP BY p.id, p.display_name, p.external_username, p.telegram_username
|
||||
ORDER BY AttendanceRate DESC, ConfirmedCount DESC
|
||||
""",
|
||||
new { GroupId = groupId })).ToList();
|
||||
}
|
||||
|
||||
public async Task LogSessionChangeAsync(Guid sessionId, long actorTelegramId, string actorName, string changeType, string? oldValue, string? newValue)
|
||||
public async Task LogSessionChangeAsync(Guid sessionId, string actorExternalUserId, string actorName, string changeType, string? oldValue, string? newValue)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
await conn.ExecuteAsync(
|
||||
"""
|
||||
INSERT INTO session_audit_log (session_id, actor_telegram_id, actor_name, change_type, old_value, new_value)
|
||||
VALUES (@SessionId, @ActorTelegramId, @ActorName, @ChangeType, @OldValue, @NewValue)
|
||||
INSERT INTO session_audit_log (session_id, actor_external_user_id, actor_name, change_type, old_value, new_value)
|
||||
VALUES (@SessionId, @ActorExternalUserId, @ActorName, @ChangeType, @OldValue, @NewValue)
|
||||
""",
|
||||
new { SessionId = sessionId, ActorTelegramId = actorTelegramId, ActorName = actorName, ChangeType = changeType, OldValue = oldValue, NewValue = newValue });
|
||||
new { SessionId = sessionId, ActorExternalUserId = actorExternalUserId, ActorName = actorName, ChangeType = changeType, OldValue = oldValue, NewValue = newValue });
|
||||
}
|
||||
|
||||
public async Task<List<SessionAuditLogEntry>> GetSessionHistoryAsync(Guid sessionId)
|
||||
@@ -220,7 +232,7 @@ public sealed class SessionService(
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
var entries = await conn.QueryAsync<SessionAuditLogEntry>(
|
||||
"""
|
||||
SELECT id, session_id AS SessionId, actor_telegram_id AS ActorTelegramId, actor_name AS ActorName,
|
||||
SELECT id, session_id AS SessionId, actor_external_user_id AS ActorExternalUserId, actor_name AS ActorName,
|
||||
change_type AS ChangeType, old_value AS OldValue, new_value AS NewValue, changed_at AS ChangedAt
|
||||
FROM session_audit_log
|
||||
WHERE session_id = @SessionId
|
||||
@@ -230,32 +242,47 @@ public sealed class SessionService(
|
||||
return entries.ToList();
|
||||
}
|
||||
|
||||
public async Task UpsertDiscordUserAsync(string discordId, string displayName, string? avatarUrl)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
await conn.ExecuteAsync(
|
||||
"""
|
||||
INSERT INTO players (display_name, platform, external_user_id, external_username)
|
||||
VALUES (@DisplayName, 'Discord', @DiscordId, @DisplayName)
|
||||
ON CONFLICT (platform, external_user_id)
|
||||
WHERE platform IS NOT NULL AND external_user_id IS NOT NULL
|
||||
DO UPDATE
|
||||
SET display_name = EXCLUDED.display_name,
|
||||
external_username = EXCLUDED.external_username
|
||||
""",
|
||||
new { DisplayName = displayName, DiscordId = discordId });
|
||||
}
|
||||
|
||||
public async Task AddGroupCoGmAsync(
|
||||
Guid groupId,
|
||||
long ownerTelegramId,
|
||||
long coGmTelegramId,
|
||||
string displayName,
|
||||
string? telegramUsername)
|
||||
string ownerPlatform, string ownerExternalUserId,
|
||||
string coGmPlatform, string coGmExternalUserId,
|
||||
string displayName, string? externalUsername)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
await using var transaction = await conn.BeginTransactionAsync();
|
||||
|
||||
await conn.ExecuteAsync(
|
||||
"""
|
||||
INSERT INTO players (telegram_id, display_name, telegram_username, platform, external_user_id, external_username)
|
||||
VALUES (@TelegramId, @DisplayName, @TelegramUsername, 'Telegram', @TelegramId::TEXT, @TelegramUsername)
|
||||
ON CONFLICT (telegram_id) DO UPDATE
|
||||
INSERT INTO players (display_name, telegram_username, platform, external_user_id, external_username)
|
||||
VALUES (@DisplayName, @ExternalUsername, @Platform, @ExternalUserId, @ExternalUsername)
|
||||
ON CONFLICT (platform, external_user_id)
|
||||
WHERE platform IS NOT NULL AND external_user_id IS NOT NULL
|
||||
DO UPDATE
|
||||
SET display_name = EXCLUDED.display_name,
|
||||
telegram_username = EXCLUDED.telegram_username,
|
||||
platform = COALESCE(players.platform, 'Telegram'),
|
||||
external_user_id = COALESCE(players.external_user_id, EXCLUDED.telegram_id::TEXT),
|
||||
external_username = COALESCE(players.external_username, EXCLUDED.telegram_username)
|
||||
external_username = EXCLUDED.external_username
|
||||
""",
|
||||
new
|
||||
{
|
||||
TelegramId = coGmTelegramId,
|
||||
DisplayName = displayName,
|
||||
TelegramUsername = telegramUsername
|
||||
ExternalUsername = externalUsername,
|
||||
Platform = coGmPlatform,
|
||||
ExternalUserId = coGmExternalUserId
|
||||
},
|
||||
transaction);
|
||||
|
||||
@@ -267,8 +294,8 @@ public sealed class SessionService(
|
||||
@CoGmRole,
|
||||
owner_player.id
|
||||
FROM players co_gm
|
||||
LEFT JOIN players owner_player ON owner_player.telegram_id = @OwnerTelegramId
|
||||
WHERE co_gm.telegram_id = @CoGmTelegramId
|
||||
LEFT JOIN players owner_player ON owner_player.platform = @OwnerPlatform AND owner_player.external_user_id = @OwnerExternalUserId
|
||||
WHERE co_gm.platform = @CoGmPlatform AND co_gm.external_user_id = @CoGmExternalUserId
|
||||
ON CONFLICT (group_id, player_id) DO UPDATE
|
||||
SET role = CASE
|
||||
WHEN group_managers.role = @OwnerRole THEN group_managers.role
|
||||
@@ -279,8 +306,10 @@ public sealed class SessionService(
|
||||
new
|
||||
{
|
||||
GroupId = groupId,
|
||||
OwnerTelegramId = ownerTelegramId,
|
||||
CoGmTelegramId = coGmTelegramId,
|
||||
OwnerPlatform = ownerPlatform,
|
||||
OwnerExternalUserId = ownerExternalUserId,
|
||||
CoGmPlatform = coGmPlatform,
|
||||
CoGmExternalUserId = coGmExternalUserId,
|
||||
OwnerRole = GroupManagerRoleExtensions.OwnerValue,
|
||||
CoGmRole = GroupManagerRoleExtensions.CoGmValue
|
||||
},
|
||||
@@ -289,7 +318,7 @@ public sealed class SessionService(
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
|
||||
public async Task RemoveGroupCoGmAsync(Guid groupId, long coGmTelegramId)
|
||||
public async Task RemoveGroupCoGmAsync(Guid groupId, string coGmPlatform, string coGmExternalUserId)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
await conn.ExecuteAsync(
|
||||
@@ -298,13 +327,15 @@ public sealed class SessionService(
|
||||
USING players p
|
||||
WHERE gm.player_id = p.id
|
||||
AND gm.group_id = @GroupId
|
||||
AND p.telegram_id = @CoGmTelegramId
|
||||
AND p.platform = @Platform
|
||||
AND p.external_user_id = @ExternalUserId
|
||||
AND gm.role = @CoGmRole
|
||||
""",
|
||||
new
|
||||
{
|
||||
GroupId = groupId,
|
||||
CoGmTelegramId = coGmTelegramId,
|
||||
Platform = coGmPlatform,
|
||||
ExternalUserId = coGmExternalUserId,
|
||||
CoGmRole = GroupManagerRoleExtensions.CoGmValue
|
||||
});
|
||||
}
|
||||
@@ -426,7 +457,7 @@ public sealed class SessionService(
|
||||
|
||||
if (oldSession is null)
|
||||
{
|
||||
throw new SessionAccessDeniedException(sessionId, 0);
|
||||
throw new SessionAccessDeniedException(sessionId, "0");
|
||||
}
|
||||
|
||||
var updatedRows = await conn.ExecuteAsync(
|
||||
@@ -454,7 +485,7 @@ public sealed class SessionService(
|
||||
|
||||
if (updatedRows == 0)
|
||||
{
|
||||
throw new SessionAccessDeniedException(sessionId, 0);
|
||||
throw new SessionAccessDeniedException(sessionId, "0");
|
||||
}
|
||||
|
||||
await conn.ExecuteAsync(
|
||||
@@ -513,7 +544,7 @@ public sealed class SessionService(
|
||||
|
||||
if (session is null)
|
||||
{
|
||||
throw new SessionAccessDeniedException(sessionId, 0);
|
||||
throw new SessionAccessDeniedException(sessionId, "0");
|
||||
}
|
||||
|
||||
var activeParticipants = await conn.ExecuteScalarAsync<int>(
|
||||
@@ -598,8 +629,10 @@ public sealed class SessionService(
|
||||
"""
|
||||
SELECT sp.id AS Id,
|
||||
p.telegram_id AS TelegramId,
|
||||
COALESCE(p.external_user_id, p.telegram_id::TEXT) AS ExternalUserId,
|
||||
p.display_name AS DisplayName,
|
||||
p.telegram_username AS TelegramUsername,
|
||||
COALESCE(p.external_username, p.telegram_username) AS ExternalUsername,
|
||||
sp.rsvp_status AS RsvpStatus,
|
||||
sp.registration_status AS RegistrationStatus,
|
||||
sp.is_gm AS IsGm,
|
||||
@@ -637,7 +670,7 @@ public sealed class SessionService(
|
||||
|
||||
if (session is null)
|
||||
{
|
||||
throw new SessionAccessDeniedException(sessionId, 0);
|
||||
throw new SessionAccessDeniedException(sessionId, "0");
|
||||
}
|
||||
|
||||
var participant = await conn.QuerySingleOrDefaultAsync<WebParticipant>(
|
||||
@@ -744,7 +777,7 @@ public sealed class SessionService(
|
||||
var batch = await GetBatchInfoAsync(conn, batchId, groupId, transaction);
|
||||
if (batch is null)
|
||||
{
|
||||
throw new SessionAccessDeniedException(batchId, 0);
|
||||
throw new SessionAccessDeniedException(batchId, "0");
|
||||
}
|
||||
|
||||
var updatedRows = await conn.ExecuteAsync(
|
||||
@@ -767,7 +800,7 @@ public sealed class SessionService(
|
||||
|
||||
if (updatedRows == 0)
|
||||
{
|
||||
throw new SessionAccessDeniedException(batchId, 0);
|
||||
throw new SessionAccessDeniedException(batchId, "0");
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
@@ -786,7 +819,7 @@ public sealed class SessionService(
|
||||
var batch = await GetBatchInfoAsync(conn, batchId, groupId, transaction);
|
||||
if (batch is null)
|
||||
{
|
||||
throw new SessionAccessDeniedException(batchId, 0);
|
||||
throw new SessionAccessDeniedException(batchId, "0");
|
||||
}
|
||||
|
||||
var updatedRows = await conn.ExecuteAsync(
|
||||
@@ -807,7 +840,7 @@ public sealed class SessionService(
|
||||
|
||||
if (updatedRows == 0)
|
||||
{
|
||||
throw new SessionAccessDeniedException(batchId, 0);
|
||||
throw new SessionAccessDeniedException(batchId, "0");
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
@@ -844,7 +877,7 @@ public sealed class SessionService(
|
||||
|
||||
if (batchSessions.Count == 0)
|
||||
{
|
||||
throw new SessionAccessDeniedException(batchId, 0);
|
||||
throw new SessionAccessDeniedException(batchId, "0");
|
||||
}
|
||||
|
||||
var newSchedule = BatchSchedulePlanner.BuildFixedIntervalSchedule(
|
||||
@@ -928,7 +961,7 @@ public sealed class SessionService(
|
||||
|
||||
if (sourceSessions.Count == 0)
|
||||
{
|
||||
throw new SessionAccessDeniedException(batchId, 0);
|
||||
throw new SessionAccessDeniedException(batchId, "0");
|
||||
}
|
||||
|
||||
var newBatchId = Guid.NewGuid();
|
||||
@@ -1130,7 +1163,7 @@ public sealed class SessionService(
|
||||
|
||||
if (template is null)
|
||||
{
|
||||
throw new SessionAccessDeniedException(templateId, 0);
|
||||
throw new SessionAccessDeniedException(templateId, "0");
|
||||
}
|
||||
|
||||
var group = await conn.QuerySingleOrDefaultAsync<WebTemplateGroupDto>(
|
||||
@@ -1140,7 +1173,7 @@ public sealed class SessionService(
|
||||
|
||||
if (group is null)
|
||||
{
|
||||
throw new SessionAccessDeniedException(groupId, 0);
|
||||
throw new SessionAccessDeniedException(groupId, "0");
|
||||
}
|
||||
|
||||
var schedule = BatchSchedulePlanner.BuildRecurringSchedule(
|
||||
|
||||
Reference in New Issue
Block a user