The admin panel already checked CSRF tokens on destructive actions, but the player-facing raw HTML forms (create/join/leave/start session, toggle ready, lobby chat, feedback) had none - cookie SameSite=Lax blunts classic cross-site auto-submit attacks but isn't a substitute for real tokens. Adds matching csrf_token()/ isCsrfTokenValid() checks to all of them. Also adds login_throttling (5 attempts/15 min) to stop unlimited password guessing, and a per-user rate limiter (10/min) on the invite-code join endpoint, since invite codes are only 32-bit and had no protection against brute-forcing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
127 lines
6.0 KiB
Twig
127 lines
6.0 KiB
Twig
{% extends 'layout/site.html.twig' %}
|
|
|
|
{% block title %}Game Won - {{ session.game.name }}{% endblock %}
|
|
|
|
{% block stylesheets %}
|
|
{{ parent() }}
|
|
<style>
|
|
.site-main {
|
|
background-color: #1a1a1a;
|
|
color: #e0e0e0;
|
|
}
|
|
.card {
|
|
background-color: #2d2d2d;
|
|
border-color: #444;
|
|
color: #e0e0e0;
|
|
}
|
|
.form-label {
|
|
font-weight: bold;
|
|
}
|
|
.story-container {
|
|
font-style: italic;
|
|
border-left: 4px solid #28a745;
|
|
padding-left: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
.donation-section {
|
|
background-color: #333;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
margin-bottom: 30px;
|
|
text-align: center;
|
|
}
|
|
.form-range::-webkit-slider-runnable-track {
|
|
background-color: #444;
|
|
}
|
|
.form-range::-moz-range-track {
|
|
background-color: #444;
|
|
}
|
|
.form-range::-webkit-slider-thumb {
|
|
background-color: #28a745;
|
|
}
|
|
.form-range::-moz-range-thumb {
|
|
background-color: #28a745;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block body %}
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-8">
|
|
<div class="card shadow-lg">
|
|
<div class="card-header bg-success text-white">
|
|
<h3 class="card-title mb-0">Game Won - Virus Contained!</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<h4>{{ session.game.name }}</h4>
|
|
<hr>
|
|
|
|
<div class="story-container">
|
|
<p>
|
|
The last protected file dissolves from the directory listing, and for a moment the mainframe goes quiet.
|
|
Then the screens flood green: containment confirmed. The AI virus's grip on the server unravels file by file.
|
|
</p>
|
|
<p>
|
|
Agents Doyle, Vega and Lennox are safe. Their identities were never decoded. In the days that follow,
|
|
the agency quietly credits an anonymous team of specialists for the save - you.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="donation-section">
|
|
<h5>Support the Developer</h5>
|
|
<p>If you enjoyed the experience, please consider a small donation to help me create more games.</p>
|
|
<a href="https://www.paypal.com/donate?hosted_button_id=X9X8KB6R6GMRU" target="_blank" class="btn btn-primary">
|
|
<i class="bi bi-paypal"></i> Donate via PayPal
|
|
</a>
|
|
</div>
|
|
|
|
<div class="feedback-form mt-4">
|
|
<h5>Feedback</h5>
|
|
<form method="post">
|
|
<input type="hidden" name="_token" value="{{ csrf_token('game_feedback_' ~ session.id) }}">
|
|
<div class="mb-3">
|
|
<label for="difficulty" class="form-label">How would you rate the difficulty? (<span id="difficulty-val">5</span>/10)</label>
|
|
<input type="range" class="form-range" min="1" max="10" step="1" id="difficulty" name="difficulty" value="5" oninput="document.getElementById('difficulty-val').innerText = this.value">
|
|
<div class="d-flex justify-content-between mt-1">
|
|
<small class="text-muted">Absolutely not</small>
|
|
-
|
|
<small class="text-muted">Absolutely</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="entertaining" class="form-label">How entertaining was it? (<span id="entertaining-val">5</span>/10)</label>
|
|
<input type="range" class="form-range" min="1" max="10" step="1" id="entertaining" name="entertaining" value="5" oninput="document.getElementById('entertaining-val').innerText = this.value">
|
|
<div class="d-flex justify-content-between mt-1">
|
|
<small class="text-muted">Absolutely not</small>
|
|
-
|
|
<small class="text-muted">Absolutely</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="theme" class="form-label">How was the theme? (<span id="theme-val">5</span>/10)</label>
|
|
<input type="range" class="form-range" min="1" max="10" step="1" id="theme" name="theme" value="5" oninput="document.getElementById('theme-val').innerText = this.value">
|
|
<div class="d-flex justify-content-between mt-1">
|
|
<small class="text-muted">Absolutely not</small>
|
|
-
|
|
<small class="text-muted">Absolutely</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="feedback" class="form-label">Additional Feedback</label>
|
|
<textarea class="form-control" id="feedback" name="feedback" rows="4" placeholder="Tell us more about your experience..."></textarea>
|
|
</div>
|
|
|
|
<div class="d-grid gap-2">
|
|
<button type="submit" class="btn btn-success">Submit Feedback & Return to Dashboard</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|