feat(web): add private club showcases with membership flow (v3.7.0)
PR Checks / test-and-build (pull_request) Successful in 7m28s
PR Checks / test-and-build (pull_request) Successful in 7m28s
Implements Issue #110: game masters can now publish sessions exclusively to a club's private showcase, gated behind a member application and approval flow. Adds a 4-state publication_mode (None/Catalog/ClubOnly/Both) replacing the binary is_public, plus a club_memberships table with Pending/Active/Rejected/Left lifecycle and partial unique index ensuring a single Active row per (group, player). Highlights - V030 migration: club_memberships, publication_mode, drop is_public, recreate partial indexes, portfolio_games gains publication_mode. - PublicationMode enum + extensions in GmRelay.Shared. - ISessionStore gains 12 membership/showcase methods; AuthorizedMembershipService owns the membership flow with GM-only approve/reject authorization. - PublicClub / PublicMasterProfile / PublicSession: member- aware queries (ClubOnly visible only to Active members). - New pages: MyClubMemberships (/profile/memberships) and ClubApplications (/group/{id}/applications). - GroupDetails and EditSession switch from a bool toggle to a 4-state publication_mode selector. - NavMenu adds Moji kluby, PublicLayout adds Kluby. Tests: 4 new test files (PublicationMode, ClubMemberships, AuthorizedMembershipService, ClubShowcaseSource) + updates to PublicClubPages, AuthorizedSessionService/Portfolio service FakeSessionStore, CampaignTemplatesNavigation. 493 tests pass. Bump version 3.6.0 -> 3.7.0 across Directory.Build.props, compose.yaml, deploy.yml, NavMenu.razor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -43,9 +43,50 @@ public sealed record WebPublicSession(
|
||||
int? MaxPlayers,
|
||||
int ActivePlayerCount,
|
||||
int WaitlistedPlayerCount,
|
||||
string PublicationMode = PublicationModeExtensions.NoneValue,
|
||||
bool IsMembersOnly = false,
|
||||
string? MasterProfileSlug = null,
|
||||
string? MasterDisplayName = null);
|
||||
|
||||
public sealed record WebMembership(
|
||||
Guid MembershipId,
|
||||
Guid GroupId,
|
||||
string GroupName,
|
||||
string? GroupSlug,
|
||||
string Status,
|
||||
string Role,
|
||||
string? Message,
|
||||
DateTime AppliedAt,
|
||||
DateTime? DecidedAt,
|
||||
string? DecidedByDisplayName);
|
||||
|
||||
public sealed record WebPendingApplication(
|
||||
Guid MembershipId,
|
||||
Guid PlayerId,
|
||||
string DisplayName,
|
||||
string Platform,
|
||||
string? ExternalUsername,
|
||||
string? Message,
|
||||
DateTime AppliedAt);
|
||||
|
||||
public sealed record WebClubShowcaseSession(
|
||||
Guid Id,
|
||||
string Title,
|
||||
DateTime ScheduledAt,
|
||||
string Status,
|
||||
string? System,
|
||||
bool IsOneShot,
|
||||
string? Format,
|
||||
int? DurationMinutes,
|
||||
string? CoverImageUrl,
|
||||
int? MaxPlayers,
|
||||
int ActivePlayerCount,
|
||||
int WaitlistedPlayerCount,
|
||||
string PublicationMode,
|
||||
bool IsMembersOnly,
|
||||
string? Description,
|
||||
bool AllowDirectRegistration);
|
||||
|
||||
public sealed record WebPublicClub(
|
||||
Guid GroupId,
|
||||
string Name,
|
||||
@@ -79,12 +120,14 @@ public interface ISessionStore
|
||||
Task<WebGameGroup?> GetGroupAsync(Guid groupId);
|
||||
Task<WebPublicGroupSettings?> GetPublicGroupSettingsAsync(Guid groupId);
|
||||
Task UpdatePublicGroupSettingsAsync(Guid groupId, string? publicSlug, bool publicScheduleEnabled);
|
||||
Task SetSessionPublicAsync(Guid sessionId, Guid groupId, bool isPublic);
|
||||
Task SetBatchPublicAsync(Guid batchId, Guid groupId, bool isPublic);
|
||||
Task<WebPublicClub?> GetPublicClubBySlugAsync(string slug);
|
||||
Task<WebPublicSession?> GetPublicSessionAsync(Guid sessionId);
|
||||
Task SetSessionPublicationModeAsync(Guid sessionId, Guid groupId, PublicationMode mode);
|
||||
Task SetBatchPublicationModeAsync(Guid batchId, Guid groupId, PublicationMode mode);
|
||||
Task<WebPublicClub?> GetPublicClubBySlugAsync(string slug, Guid? viewerPlayerId);
|
||||
Task<WebPublicSession?> GetPublicSessionAsync(Guid sessionId, Guid? viewerPlayerId);
|
||||
Task<bool> IsGroupManagerAsync(Guid groupId, string platform, string externalUserId);
|
||||
Task<bool> IsGroupOwnerAsync(Guid groupId, string platform, string externalUserId);
|
||||
Task<bool> IsActiveClubMemberAsync(Guid groupId, Guid playerId);
|
||||
Task<Guid?> GetPlayerIdByPlatformIdentityAsync(string platform, string externalUserId);
|
||||
Task<List<WebGroupManager>> GetGroupManagersAsync(Guid groupId);
|
||||
Task<List<WebSession>> GetUpcomingSessionsAsync(Guid groupId);
|
||||
Task<WebSession?> GetSessionAsync(Guid sessionId);
|
||||
@@ -110,7 +153,7 @@ public interface ISessionStore
|
||||
Task UpsertDiscordUserAsync(string discordId, string displayName, string? avatarUrl);
|
||||
Task<MasterProfileSettings?> GetMasterProfileSettingsAsync(string platform, string externalUserId);
|
||||
Task UpdateMasterProfileSettingsAsync(string platform, string externalUserId, string? publicSlug, bool isPublic, string displayName, string? bio);
|
||||
Task<PublicMasterProfile?> GetPublicMasterProfileBySlugAsync(string slug);
|
||||
Task<PublicMasterProfile?> GetPublicMasterProfileBySlugAsync(string slug, Guid? viewerPlayerId);
|
||||
|
||||
// --- Identity linking (issue #35) ---
|
||||
Task<Guid?> ResolveEffectivePlayerIdAsync(string platform, string externalUserId);
|
||||
@@ -123,6 +166,17 @@ public interface ISessionStore
|
||||
Task<IReadOnlyList<ShowcaseSessionDto>> GetShowcaseSessionsAsync(ShowcaseFilter filter, int page, int pageSize);
|
||||
Task<ShowcaseSessionDto?> GetShowcaseSessionAsync(Guid sessionId);
|
||||
Task<bool> RegisterFromShowcaseAsync(Guid sessionId, string platform, string externalUserId, string displayName);
|
||||
|
||||
// --- Private club showcases / memberships (issue #110) ---
|
||||
Task<IReadOnlyList<WebClubShowcaseSession>> GetClubShowcaseSessionsAsync(Guid groupId, Guid? viewerPlayerId, int page, int pageSize);
|
||||
Task<int> GetPendingApplicationsCountAsync(Guid groupId);
|
||||
Task<List<WebPendingApplication>> GetPendingApplicationsAsync(Guid groupId);
|
||||
Task<List<WebMembership>> GetMembershipsForPlayerAsync(Guid playerId);
|
||||
Task<Guid> ApplyForMembershipAsync(Guid groupId, Guid playerId, string? message);
|
||||
Task ApproveMembershipAsync(Guid membershipId, Guid approverPlayerId);
|
||||
Task RejectMembershipAsync(Guid membershipId, Guid approverPlayerId);
|
||||
Task LeaveClubMembershipAsync(Guid membershipId, Guid playerId);
|
||||
Task<Guid?> GetGroupIdForMembershipAsync(Guid membershipId);
|
||||
}
|
||||
|
||||
public sealed record LinkedIdentity(
|
||||
|
||||
Reference in New Issue
Block a user