feat: admin can answer questions without paying for testing

- Server: /api/respuesta/enviar checks admin cookie; if authorized,
  bypasses credit check and stores answer via insertAdminAnswer()
- DB: insertAdminAnswer() inserts directly into user_answers with
  username='Admin', skipping the credit budget entirely
- Frontend: ProposeAnswer checks /api/admin/status on mount; if admin
  is logged in, shows the answer form directly (orange Admin badge)
  instead of the payment tier selection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 18:15:46 +01:00
parent f9a8e2544f
commit fe5bb5a5c2
3 changed files with 88 additions and 17 deletions

View File

@@ -7,7 +7,8 @@ import broadcastHtml from "./broadcast.html";
import preguntaHtml from "./pregunta.html";
import {
clearAllRounds, getRounds, getAllRounds,
createPendingCredit, activateCredit, getCreditByOrder, submitUserAnswer,
createPendingCredit, activateCredit, getCreditByOrder,
submitUserAnswer, insertAdminAnswer,
getPlayerScores,
} from "./db.ts";
import { buildPaymentForm, verifyNotification, decodeParams, isPaymentApproved } from "./redsys.ts";
@@ -706,7 +707,9 @@ const server = Bun.serve<WsData>({
return new Response("Invalid JSON body", { status: 400 });
}
if (!token) {
const adminMode = isAdminAuthorized(req, url);
if (!token && !adminMode) {
return new Response("Token requerido", { status: 401 });
}
if (text.length < 3 || text.length > 150) {
@@ -718,17 +721,28 @@ const server = Bun.serve<WsData>({
return new Response("No hay ronda activa", { status: 409 });
}
const result = submitUserAnswer(token, round.num, text);
if (!result) {
return new Response("Crédito no válido o sin respuestas disponibles", { status: 401 });
let username: string;
let answersLeft: number;
if (adminMode) {
username = "Admin";
insertAdminAnswer(round.num, text, username);
answersLeft = 999;
} else {
const result = submitUserAnswer(token, round.num, text);
if (!result) {
return new Response("Crédito no válido o sin respuestas disponibles", { status: 401 });
}
username = result.username;
answersLeft = result.answersLeft;
}
// Add to live round state and broadcast
round.userAnswers = [...(round.userAnswers ?? []), { username: result.username, text }];
round.userAnswers = [...(round.userAnswers ?? []), { username, text }];
broadcast();
log("INFO", "respuesta", "User answer submitted", { username: result.username, round: round.num, ip });
return new Response(JSON.stringify({ ok: true, answersLeft: result.answersLeft }), {
log("INFO", "respuesta", "Answer submitted", { username, round: round.num, admin: adminMode, ip });
return new Response(JSON.stringify({ ok: true, answersLeft }), {
status: 200,
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" },
});