Multiple admins

Adds Player.is_admin (creator starts with it; backfilled by migration).
Admin-key-authenticated POST /admin/set-admin grants/revokes it from the
admin panel; the creator's privileges can't be revoked. Admin-gated
backend routes (dev mode, skip character creation) and frontend controls
(corner menu, lobby start, scene start, end story) now check is_admin
instead of is_creator, and admins get the admin_key in their state blob
so granted players can open the admin panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 16:03:14 -07:00
parent e24e6d3b46
commit 5ad9c9db07
12 changed files with 203 additions and 27 deletions

View File

@@ -40,6 +40,19 @@
data = null;
}
}
async function setAdmin(playerId, makeAdmin) {
try {
await apiRequest(`/game/${gameId}/admin/set-admin`, 'POST', {
key: adminKey,
target_player_id: playerId,
is_admin: makeAdmin,
});
await loadAdminData();
} catch (err) {
error = err.message;
}
}
</script>
<div class="admin-page">
@@ -71,6 +84,7 @@
<th>Role</th>
<th>Status</th>
<th>Rank</th>
<th>Admin</th>
<th>Re-join Link</th>
</tr>
</thead>
@@ -92,6 +106,15 @@
{/if}
</td>
<td>{p.rank}</td>
<td>
{#if p.is_creator}
🗝 Creator
{:else if p.is_admin}
<button on:click={() => setAdmin(p.id, false)} class="btn btn-secondary btn-small">Revoke Admin</button>
{:else}
<button on:click={() => setAdmin(p.id, true)} class="btn btn-secondary btn-small">Grant Admin</button>
{/if}
</td>
<td>
<div class="link-copy-action">
<a href={rejoinLink(p.id)} class="btn btn-secondary btn-small">Open</a>