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:
@@ -1216,7 +1216,12 @@ def test_admin_key_returned_at_create_but_hidden_from_state():
|
||||
assert created["admin_key"]
|
||||
|
||||
game_id = created["id"]
|
||||
player_id = client.post(f"/api/game/{game_id}/join", data={"name": "Carlos"}).json()["id"]
|
||||
# First joiner is the creator (an Admin), so they DO see the key
|
||||
creator_id = client.post(f"/api/game/{game_id}/join", data={"name": "Carlos"}).json()["id"]
|
||||
state = client.get(f"/api/game/{game_id}/player/{creator_id}/state").json()
|
||||
assert state["game"]["admin_key"] == created["admin_key"]
|
||||
# Regular players don't
|
||||
player_id = client.post(f"/api/game/{game_id}/join", data={"name": "Swabbie"}).json()["id"]
|
||||
state = client.get(f"/api/game/{game_id}/player/{player_id}/state").json()
|
||||
assert "admin_key" not in state["game"]
|
||||
finally:
|
||||
@@ -1356,3 +1361,75 @@ def test_dev_mode_default_env(session, monkeypatch):
|
||||
assert crud.create_game(session).dev_mode is False
|
||||
monkeypatch.setenv("PIRATS_DEV_MODE", "true")
|
||||
assert crud.create_game(session).dev_mode is True
|
||||
|
||||
def test_grant_and_revoke_admin():
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from sqlmodel import SQLModel, create_engine, Session
|
||||
from pirats.main import app
|
||||
from pirats import crud
|
||||
|
||||
engine = create_engine(
|
||||
"sqlite://",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
session = Session(engine)
|
||||
|
||||
game = crud.create_game(session)
|
||||
creator = crud.add_player(session, game.id, "Creator", is_creator=True)
|
||||
mate = crud.add_player(session, game.id, "Mate")
|
||||
assert creator.is_admin
|
||||
assert not mate.is_admin
|
||||
|
||||
def get_session_override():
|
||||
yield session
|
||||
|
||||
from pirats.database import get_session
|
||||
app.dependency_overrides[get_session] = get_session_override
|
||||
client = TestClient(app)
|
||||
|
||||
try:
|
||||
# Admin-only endpoints reject non-admins
|
||||
response = client.post(f"/api/game/{game.id}/player/{mate.id}/dev-mode", data={"enabled": True})
|
||||
assert response.status_code == 403
|
||||
|
||||
# Wrong key can't grant
|
||||
response = client.post(f"/api/game/{game.id}/admin/set-admin",
|
||||
data={"key": "nope", "target_player_id": mate.id, "is_admin": True})
|
||||
assert response.status_code == 403
|
||||
|
||||
# Grant with the real key
|
||||
response = client.post(f"/api/game/{game.id}/admin/set-admin",
|
||||
data={"key": game.admin_key, "target_player_id": mate.id, "is_admin": True})
|
||||
assert response.status_code == 200
|
||||
session.refresh(mate)
|
||||
assert mate.is_admin
|
||||
|
||||
# The new admin can use admin-only endpoints and sees the admin key in state
|
||||
response = client.post(f"/api/game/{game.id}/player/{mate.id}/dev-mode", data={"enabled": True})
|
||||
assert response.status_code == 200
|
||||
response = client.get(f"/api/game/{game.id}/player/{mate.id}/state")
|
||||
assert response.json()["game"]["admin_key"] == game.admin_key
|
||||
# ...but non-admins still don't
|
||||
response = client.get(f"/api/game/{game.id}/player/{creator.id}/state")
|
||||
assert "admin_key" in response.json()["game"] # creator is admin too
|
||||
|
||||
# Revoke works on regular admins, never on the creator
|
||||
response = client.post(f"/api/game/{game.id}/admin/set-admin",
|
||||
data={"key": game.admin_key, "target_player_id": mate.id, "is_admin": False})
|
||||
assert response.status_code == 200
|
||||
session.refresh(mate)
|
||||
assert not mate.is_admin
|
||||
response = client.post(f"/api/game/{game.id}/admin/set-admin",
|
||||
data={"key": game.admin_key, "target_player_id": creator.id, "is_admin": False})
|
||||
assert response.status_code == 400
|
||||
session.refresh(creator)
|
||||
assert creator.is_admin
|
||||
|
||||
# Once revoked, the state blob hides the key again
|
||||
response = client.get(f"/api/game/{game.id}/player/{mate.id}/state")
|
||||
assert "admin_key" not in response.json()["game"]
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
Reference in New Issue
Block a user