From 9da09315ea87ed6cdb437ae9a8024795f08da02b Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Fri, 6 Feb 2026 19:36:44 +0100 Subject: [PATCH] Lock display names at registration --- API.md | 2 +- Contracts/Dtos.cs | 2 -- Endpoints/StateEndpoints.cs | 27 --------------------------- FAQ.md | 31 ++++++++++++++----------------- GameList.Tests/StateTests.cs | 25 +++++++------------------ TESTS.md | 4 ++-- 6 files changed, 24 insertions(+), 67 deletions(-) diff --git a/API.md b/API.md index 1bdf6b7..06f581d 100644 --- a/API.md +++ b/API.md @@ -6,13 +6,13 @@ All endpoints are JSON. Most routes require the HttpOnly `player` cookie issued POST /api/auth/register — accepts optional `adminKey` to set `IsAdmin=true` POST /api/auth/login POST /api/auth/logout +Display names are set during registration and are immutable afterward. ## State (requires auth) GET /api/state — returns currentPhase (for caller), votesFinal, resultsOpen, updatedAt, counts (players/suggestions/votes) GET /api/me — id, displayName, username, isAdmin, currentPhase, votesFinal ## Player (requires auth) -POST /api/me/name — set display name (max 16 chars) POST /api/me/phase/next — advance caller to next phase (Suggest→Vote→Results; Results gated by resultsOpen) POST /api/me/phase/prev — admin-only move caller backward (Results→Vote→Suggest) diff --git a/Contracts/Dtos.cs b/Contracts/Dtos.cs index c6b108d..995be2e 100644 --- a/Contracts/Dtos.cs +++ b/Contracts/Dtos.cs @@ -2,8 +2,6 @@ using GameList.Domain; namespace GameList.Contracts; -public record SetNameRequest(string Name); - public record SuggestionRequest(string Name, string? Genre, string? Description, string? ScreenshotUrl, string? YoutubeUrl, string? GameUrl, int? MinPlayers, int? MaxPlayers); public record SuggestionDto(int Id, string Name, string? Genre, string? Description, string? ScreenshotUrl, string? YoutubeUrl, string? GameUrl, int? MinPlayers, int? MaxPlayers, int? ParentSuggestionId = null, IReadOnlyList? LinkedIds = null, IReadOnlyList? LinkedTitles = null); diff --git a/Endpoints/StateEndpoints.cs b/Endpoints/StateEndpoints.cs index d5b3869..a276986 100644 --- a/Endpoints/StateEndpoints.cs +++ b/Endpoints/StateEndpoints.cs @@ -1,8 +1,6 @@ -using GameList.Contracts; using GameList.Data; using GameList.Domain; using Microsoft.EntityFrameworkCore; -using Microsoft.AspNetCore.Mvc; namespace GameList.Endpoints; @@ -101,31 +99,6 @@ public static class StateEndpoints }); }); - group.MapPost("/me/name", async ([FromBody] SetNameRequest request, HttpContext ctx, AppDbContext db) => - { - if (request.Name.Trim().Length > 16) - { - return Results.BadRequest(new { error = "Name is required and must be <= 16 characters." }); - } - - var name = EndpointHelpers.TrimTo(request.Name, 16); - if (string.IsNullOrWhiteSpace(name)) - { - return Results.BadRequest(new { error = "Name is required and must be <= 16 characters." }); - } - - var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db); - if (player is null) - return Results.Unauthorized(); - - player.DisplayName = name; - await db.SaveChangesAsync(); - return Results.Ok(new - { - player.Id, - player.DisplayName - }); - }); } private static Phase NextPhase(Phase current) => current switch diff --git a/FAQ.md b/FAQ.md index a343a5b..3638c4d 100644 --- a/FAQ.md +++ b/FAQ.md @@ -1,34 +1,33 @@ # FAQ & Tips -This page explains how Pick'n'Play works in plain language. Each answer includes the small rules the backend enforces so you know what to expect. +Pick'n'Play helps a small group collect game ideas, score them, and reveal a ranked list without endless back-and-forth. Everything below is written in plain language and reflects the exact rules the backend enforces. ## Accounts & login - **How do I create an account?** Register with a unique username (max 24 characters), a password, and a display name (max 16 characters). The display name is required because it is shown beside every suggestion and score. -- **What if I need admin powers?** During registration, enter the admin key your host shared. If the key is wrong the request is rejected; there is no way to “upgrade” yourself later without re‑registering with the correct key. -- **Can I change my display name later?** Yes. Use the profile/name action in the app; it updates immediately and is required before you can suggest or vote. -- **Why does login sometimes fix my missing name?** If an old account somehow lost its display name, logging in auto-fills it from your username the next time you sign in. +- **Do I need admin powers?** If you were provided with an admin key, enter it during registration. If the key is wrong the request is rejected; there is no way to “upgrade” yourself later without re‑registering with the correct key. +- **Can I change my display name later?** No. Pick it during registration and keep it; there is no UI or API to change it afterward. (Only legacy accounts that somehow lost a name are auto-filled from the username on next login.) ## Phases at a glance - **Personal phases.** Each player tracks their own position (Suggest → Vote → Results). Clicking “Next” moves you forward; admins can move themselves backward. Moving forward always clears any “finalized” vote flag. -- **Results gate.** You cannot enter Results until an admin opens them. When results are opened, everyone is pushed to Results automatically. When results are closed, everyone returns to Vote and all ballots are unfinalized so people can adjust scores again. +- **Results gate.** You cannot enter Results until an admin opens them. When Results are opened, everyone is pushed to Results automatically. When Results are closed, everyone returns to Vote and all ballots are unfinalized so people can adjust scores again. ## Suggesting games -- **How many can I add?** Up to 5 suggestions per player while you are in the Suggest phase. An admin-granted “joker” lets you add one extra game during Vote (details below). -- **Required fields and limits.** Title is required (max 100 characters). Genre (50), description (500), and links (URLs up to 2048) are optional. Min/max players must both be provided or both left empty; values must be between 1 and 32 with min ≤ max. +- **How many can I add?** Up to 5 suggestions per player while you are in the Suggest phase. An admin-granted joker lets you add one extra game during Vote (see below). +- **Required fields and limits.** Name is required (max 100 characters). Genre (50), description (500), and links (URLs up to 2048) are optional. Min/max players must both be provided or both left empty; values must be between 1 and 32 with min ≤ max. - **Image rules.** Screenshot URLs must be http/https, end with an image extension (png, jpg, jpeg, gif, webp, avif), be publicly reachable within ~3 seconds, avoid redirects, and be under 5 MB. Links to local/private hosts are rejected for safety. If you omit a screenshot, that’s fine. - **Other links.** Game and YouTube links must be http or https; any other scheme is rejected. -- **When can I edit?** In Suggest you may edit any field. In Vote you may edit everything except the title (it stays locked). In Results you cannot edit. Admins can edit any time. -- **Deleting a suggestion.** You can delete your own suggestions while you are still in Suggest. Admins can delete at any time. Deleting a game also removes its votes and breaks any duplicate links so nobody gets stuck with orphaned data. -- **Why was my suggestion blocked?** Common reasons: no display name, already at the 5-limit and no joker, title too long/empty, invalid or unreachable screenshot, min/max players missing or out of order, or trying to add while in the wrong phase. +- **What can I edit?** In Suggest you may edit any field. In Vote you may edit everything except the name (it stays locked). In Results nothing is editable. Admins can edit any suggestion at any time. +- **Deleting a suggestion.** You can delete your own suggestions while you are still in Suggest. Admins can delete any suggestion at any time. Deleting also removes its votes and breaks any duplicate links so nobody is stuck with orphaned data. +- **Why was my suggestion blocked?** Common reasons: no display name at registration, already at the 5-limit and no joker, name too long/empty, invalid or unreachable screenshot, min/max players missing or out of order, or trying to add while in the wrong phase. ## Jokers (late additions) - **What is a joker?** A one-time extra suggestion slot that only works while you are in the Vote phase. An admin must grant it to you. -- **How it works.** When you use the joker to add a game, the joker is consumed immediately, your ballot is unfinalized, and every player is also unfinalized so the new game can be scored. Admins can grant you another joker later if needed. +- **How it works.** When you use the joker to add a game, the joker is consumed immediately, your ballot is unfinalized, and every player is also unfinalized so the new game can be scored. Admins can grant another joker later if needed. ## Voting -- **Who can vote?** Only authenticated players in the Vote phase with a display name set. If you removed your name, set it again before voting. +- **Who can vote?** Only authenticated players in the Vote phase. Because display names are fixed at registration, there is no “set name first” step during voting. - **How to score.** Use the slider to pick a whole number from 0 to 10. Anything outside that range is rejected. -- **Linked duplicates.** If an admin linked duplicate games, changing the score on one updates all linked siblings automatically. Scores are stored per group, not per card. +- **Linked duplicates.** If an admin linked duplicate games, changing the score on one updates all linked siblings automatically. Scores are stored per linked group, not per individual card. - **Finalizing.** Toggling “Finalize” locks your scores so you cannot change them. Toggle it off to edit again. Finalize is only available in Vote; it is cleared automatically if a joker adds a new game, if an admin links/unlinks games, or if results are closed and you are sent back to Vote. - **Voting after changes.** When new games appear or links change, your previous votes for the affected group are cleared and you are unfinalized. Check your list and rescore everything before finalizing again. @@ -38,7 +37,7 @@ This page explains how Pick'n'Play works in plain language. Each answer includes - **Can I change anything here?** No. Suggestions and votes are read-only in Results. Admins must close results (which returns everyone to Vote) or use admin tools to make changes. ## Admin tools (for hosts) -- Open/close results; closing results moves everyone back to Vote and clears finalize flags. +- Open or close results; closing results moves everyone back to Vote and clears finalize flags. - Grant jokers during Vote; granting one also clears that player’s finalize flag. - Link or unlink duplicate suggestions during Vote; this clears votes for the affected group and unfinalizes impacted players. - View vote readiness to see who has finalized. @@ -48,11 +47,9 @@ This page explains how Pick'n'Play works in plain language. Each answer includes ## Common errors and how to fix them - **“This endpoint is available in the X phase.”** You are in the wrong phase. Advance with “Next” or ask an admin to move you back. -- **“Set a display name…”** Add or restore your display name before suggesting or voting. - **“Screenshot URL must be http(s) and end with an image file extension.”** Fix the extension and make sure it is a direct link, not a page or redirect, and the file is smaller than 5 MB. -- **“Score must be between 0 and 10.”** Pick a whole number inside that range. - **“You have reached the 5 suggestion limit.”** Wait for Vote and request a joker if you need to add more. -- **Admin key errors.** If you see “Invalid admin key,” register again with the correct key provided by the host. +- **Admin key errors.** If you see “Invalid admin key,” register again with the correct key provided by the host, or leave it empty to create a non-admin account. ## Data and privacy - Suggestions, votes, and phases live in a shared SQLite database. Logging out clears your auth cookie; deleting your player (admin action) also removes your suggestions and votes. diff --git a/GameList.Tests/StateTests.cs b/GameList.Tests/StateTests.cs index 5ccae8f..3b637a8 100644 --- a/GameList.Tests/StateTests.cs +++ b/GameList.Tests/StateTests.cs @@ -136,17 +136,6 @@ public class StateTests Assert.False(me.GetProperty("votesFinal").GetBoolean()); } - [Fact] - public async Task Name_endpoint_rejects_over_16_chars() - { - await using var factory = new TestWebApplicationFactory(); - var client = factory.CreateClientWithCookies(); - await client.RegisterAsync("namelimit"); - - var resp = await client.PostAsJsonAsync("/api/me/name", new { name = new string('a', 17) }); - Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode); - } - [Fact] public async Task Cannot_advance_to_results_when_locked() { @@ -182,19 +171,19 @@ public class StateTests } [Fact] - public async Task Name_endpoint_trims_and_rejects_blank() + public async Task Display_name_cannot_be_changed_after_registration() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); - await client.RegisterAsync("nametest"); + var username = "fixedname"; + await client.RegisterAsync(username); + var originalDisplay = $"{username}-name"; - var bad = await client.PostAsJsonAsync("/api/me/name", new { name = " " }); - Assert.Equal(HttpStatusCode.BadRequest, bad.StatusCode); + var attempt = await client.PostAsJsonAsync("/api/me/name", new { name = "New Name" }); + Assert.Equal(HttpStatusCode.NotFound, attempt.StatusCode); - var ok = await client.PostAsJsonAsync("/api/me/name", new { name = " Alice " }); - ok.EnsureSuccessStatusCode(); var me = await client.GetFromJsonAsync("/api/me"); - Assert.Equal("Alice", me.GetProperty("displayName").GetString()); + Assert.Equal(originalDisplay, me.GetProperty("displayName").GetString()); } [Fact] diff --git a/TESTS.md b/TESTS.md index 96f086c..c9c4f02 100644 --- a/TESTS.md +++ b/TESTS.md @@ -7,7 +7,7 @@ Purpose: full coverage of backend + critical UI flows using a mock (in-memory) S | Role | Suggest phase | Vote phase | Results phase | Anytime | | --- | --- | --- | --- | --- | | Unauthenticated visitor | No API access; only static assets | — | — | Health check only | -| Player (non-admin) | Create/see own suggestions (≤5), edit all fields, delete own; can advance to Vote; title locks after leaving phase | View all suggestions, vote 0–10, finalize/unfinalize, use joker once to add a game; cannot go backward | Read leaderboard only when resultsOpen=true; no writes | Login/logout, set display name, read /state and /me | +| Player (non-admin) | Create/see own suggestions (≤5), edit all fields, delete own; can advance to Vote; title locks after leaving phase | View all suggestions, vote 0–10, finalize/unfinalize, use joker once to add a game; cannot go backward | Read leaderboard only when resultsOpen=true; no writes | Login/logout, read /state and /me | | Admin (isAdmin=true) | Same as player; may edit/delete any suggestion | All player actions; may grant jokers, link/unlink games, delete players | Open/close results; sees leaderboard like player | Toggle results, reset/factory-reset DB, fetch vote status, move self backward | ## Phase/Permission Chart (for tests) @@ -43,7 +43,7 @@ stateDiagram-v2 - GetPhase auto-upgrades legacy Reveal -> Vote and realigns when resultsOpen toggles (to Results and back to Vote clearing votesFinal). - /me/phase/next: moves Suggest->Vote, Vote->Results only when resultsOpen true; clears votesFinal; rejects when results locked. - /me/phase/prev: admin only; moves back one step, clears votesFinal, rejects for player. -- /me/name: trims/limits to 16, rejects blank; persists change. +- Display name is immutable after registration; attempts to change via /api/me/name return 404. ### 3) Suggestions - GET /mine returns only caller’s suggestions ordered by CreatedAt.