Skip to content

Commit a45bf77

Browse files
authored
Allow hosts to remove other hosts, add session guard to admin page
- The voter list remove button now shows for all voters except the currently logged-in user (previously hidden for all hosts). Hosts can now be kicked by other hosts, but cannot kick themselves. - getSessionIds now throws on non-OK responses, consistent with other API helpers. - The admin page gains the same sessionValid session-check pattern used on the meeting page: a refreshSession callback polls /api/common/vote-progress every 10 seconds and on 401 sets the session invalid. The initial data load also detects 401 immediately. When the session is invalid, a "Not an administrator" panel is shown instead of the normal admin UI which is necessary because a host can now be removed mid-session by another host.
1 parent b3f535b commit a45bf77

2 files changed

Lines changed: 65 additions & 3 deletions

File tree

frontend/src/routes/admin.tsx

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import { Badge } from "@/components/Badge/Badge";
1010
import { Panel } from "@/components/Panel/Panel";
1111
import { VotePanel, type VoteState } from "@/components/VotePanel/VotePanel";
1212
import {
13+
apiFetch,
1314
apiUrl,
1415
startVoteRound,
1516
tally as tallyVote,
1617
getTally,
1718
endVoteRound,
19+
getSessionIds,
1820
type TallyResult,
1921
} from "@/signatures/voteSession";
2022
import {
@@ -224,12 +226,14 @@ function QRPanel({
224226
function VoterListPanel({
225227
voters,
226228
loading,
229+
selfUuuid,
227230
onRemove,
228231
onRemoveAll,
229232
onReload,
230233
}: {
231234
voters: VoterInfo[];
232235
loading: boolean;
236+
selfUuuid: string | null;
233237
onRemove: (uuid: string) => Promise<void>;
234238
onRemoveAll: () => Promise<void>;
235239
onReload: () => void;
@@ -365,7 +369,7 @@ function VoterListPanel({
365369
</Badge>
366370
)}
367371

368-
{!v.is_host && (
372+
{v.uuid !== selfUuuid && (
369373
<button
370374
type="button"
371375
onClick={() => handleRemove(v.uuid)}
@@ -922,6 +926,8 @@ function HostVoteRoundPanel({
922926

923927
// ─── Admin page ───────────────────────────────────────────────────────────────
924928

929+
const SESSION_POLL_MS = 10_000;
930+
925931
function Admin() {
926932
const navigate = useNavigate();
927933
const [voters, setVoters] = useState<VoterInfo[]>([]);
@@ -942,6 +948,9 @@ function Admin() {
942948
null,
943949
);
944950
const [loadError, setLoadError] = useState<string | null>(null);
951+
const [selfUuuid, setSelfUuuid] = useState<string | null>(null);
952+
// null = still checking, true = in meeting, false = removed/not logged in
953+
const [sessionValid, setSessionValid] = useState<boolean | null>(null);
945954

946955
const reloadVoters = useCallback(async () => {
947956
try {
@@ -952,23 +961,46 @@ function Admin() {
952961
}
953962
}, []);
954963

964+
const refreshSession = useCallback(async () => {
965+
let res: Response;
966+
try {
967+
res = await apiFetch("/api/common/vote-progress");
968+
} catch {
969+
return; // network error — don't invalidate session
970+
}
971+
if (res.status === 401) {
972+
setSessionValid(false);
973+
return;
974+
}
975+
if (!res.ok) return; // other server error — keep current session state
976+
setSessionValid(true);
977+
}, []);
978+
955979
// ── Initial load ────────────────────────────────────────────────────────────
956980
useEffect(() => {
957981
async function init() {
958982
try {
959-
const [voterList, progress] = await Promise.all([
983+
const [voterList, progress, sessionIds] = await Promise.all([
960984
fetchVoterList(),
961985
fetchVoteProgress(),
986+
getSessionIds(),
962987
]);
963988
setVoters(voterList);
989+
setSelfUuuid(sessionIds.uuuid);
964990
setVoteProgress(progress);
965991
const state = deriveVoteState(progress);
966992
setVoteState(state);
993+
setSessionValid(true);
967994
if (state === "Tally") {
968995
getTally().then(setTallyResult).catch(console.error);
969996
}
970997
} catch (err) {
971-
setLoadError(String(err));
998+
if (err instanceof Error && err.message.includes("401")) {
999+
setSessionValid(false);
1000+
} else {
1001+
setSessionValid(true);
1002+
setLoadError(String(err));
1003+
}
9721004
} finally {
9731005
setVotersLoading(false);
9741006
}
@@ -1048,6 +1080,12 @@ function Admin() {
10481080
return () => es.close();
10491081
}, [reloadVoters]);
10501082

1083+
// ── Periodic session check ───────────────────────────────────────────────────
1084+
useEffect(() => {
1085+
const timer = setInterval(refreshSession, SESSION_POLL_MS);
1086+
return () => clearInterval(timer);
1087+
}, [refreshSession]);
1088+
10511089
// ── Handlers ────────────────────────────────────────────────────────────────
10521090

10531091
async function handleStartVote(
@@ -1125,6 +1163,28 @@ function Admin() {
11251163

11261164
// ── Render ──────────────────────────────────────────────────────────────────
11271165

1166+
if (sessionValid === null) {
1167+
return (
1168+
<div className="flex items-center justify-center py-32">
1169+
<Spinner size="l" color="primary" />
1170+
</div>
1171+
);
1172+
}
1173+
1174+
if (!sessionValid) {
1175+
return (
1176+
<div className="max-w-md mx-auto px-6 py-10">
1177+
<Panel title="Not an administrator">
1178+
<p className="text-sm" style={{ color: "var(--textSecondary)" }}>
1179+
You are not currently an administrator in a meeting. You may have
1180+
been removed or your session may have expired. If you believe this
1181+
is a mistake, please contact your meeting administrator.
1182+
</p>
1183+
</Panel>
1184+
</div>
1185+
);
1186+
}
1187+
11281188
if (loadError) {
11291189
return (
11301190
<div className="max-w-xl mx-auto p-8">
@@ -1287,6 +1347,7 @@ function Admin() {
12871347
<VoterListPanel
12881348
voters={voters}
12891349
loading={votersLoading}
1350+
selfUuuid={selfUuuid}
12901351
onRemove={removeVoter}
12911352
onRemoveAll={removeAllVoters}
12921353
onReload={reloadVoters}

frontend/src/signatures/voteSession.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export interface SessionIds {
8080

8181
export async function getSessionIds(): Promise<SessionIds> {
8282
const res = await apiFetch("/api/session-ids");
83+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
8384
const { uuuid, muuid } = await res.json();
8485

8586
return { uuuid, muuid };

0 commit comments

Comments
 (0)