7 Commits
Author SHA1 Message Date
Frank 03994dd54e Merge pull request 'Registration' (#2) from Registration into main
Reviewed-on: #2
2026-01-05 11:13:03 +00:00
Frank 499e699dbd Forgot password 2026-01-03 22:57:45 +01:00
Frank c8b0a6e966 Maillog 2026-01-03 22:35:56 +01:00
Frank 5b6bfaf5ad Quite some work done here. 2026-01-03 22:12:51 +01:00
Frank af61a3b920 Some settings 2026-01-03 13:16:58 +01:00
Frank 0d6628e7c9 Startup 2026-01-02 20:27:56 +01:00
Frank 534175efb3 Setup 2025-09-06 16:50:16 +02:00
215 changed files with 1550 additions and 11492 deletions
-25
View File
@@ -1,25 +0,0 @@
# Git
.git
.gitignore
# Symfony
var/cache/*
var/log/*
var/sessions/*
!var/cache/.gitkeep
!var/log/.gitkeep
!var/sessions/.gitkeep
# Node
node_modules
npm-debug.log
# Other
.env.local
.env.local.php
.env.dev.local
.env.test.local
.env.prod.local
vendor
public/build
+18 -43
View File
@@ -11,37 +11,21 @@
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Copy this file to .env (and .env.dev / .env.prod / .env.test as needed) and
# fill in real values. Those files are gitignored and never committed.
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=prod
APP_SECRET=CHANGEME_APP_SECRET
TRUSTED_PROXIES=127.0.0.1,172.20.0.1,172.20.0.0/16
TRUSTED_HOSTS=^.*$
APP_ENV=dev
APP_SECRET=
###< symfony/framework-bundle ###
SITE_BASE_URL=https://escapepage.com
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
DB_DRIVER=pdo_mysql
DB_SERVER_VERSION=8.0.32
DB_CHARSET=utf8mb4
DB_USER=escapepage
DB_PASSWORD=CHANGEME_DB_PASSWORD
DB_HOST=database
DB_PORT=3306
DB_NAME=escapepage
MYSQL_ROOT_PASSWORD=CHANGEME_MYSQL_ROOT_PASSWORD
DATABASE_URL="${DB_DRIVER}://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?serverVersion=${DB_SERVER_VERSION}&charset=${DB_CHARSET}"
DATABASE_URL="mysql://escapepage:b.0nqrxJ%%2FD%%2ALuf9N@localhost:3306/escapepage?serverVersion=8.0.32&charset=utf8mb4"
###< doctrine/doctrine-bundle ###
###> symfony/messenger ###
@@ -53,36 +37,27 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###> symfony/mailer ###
# Development: use Mailpit (docker compose override provides service `mailer` on port 1025)
MAILGUN_API_KEY=REPLACE_WITH_MAILGUN_API_KEY
MAILGUN_DOMAIN=REPLACE_WITH_MAILGUN_SENDING_DOMAIN
MAILER_DSN=mailgun+api://${MAILGUN_API_KEY}:${MAILGUN_DOMAIN}@default?region=eu
MAILER_FROM=mailer@escapepage.nl
MAILER_DSN=smtp://mailer:1025
# Production/Stage (uncomment and set SENDGRID_API_KEY in real env or secrets):
# MAILER_DSN=sendgrid+api://%env(SENDGRID_API_KEY)%
# Alternatively, via SMTP (no extra package needed):
# MAILER_DSN="smtp://apikey:%env(SENDGRID_API_KEY)%@smtp.sendgrid.net:587?encryption=tls"
# Optional default sender (used by test command if --from not passed):
# MAILER_FROM=no-reply@your-domain.tld
# SENDGRID_API_KEY=your_real_key_goes_here # Do NOT commit this; set in .env.local or deployment env
###< symfony/mailer ###
###> symfony/sendgrid-mailer ###
# MAILER_DSN=sendgrid://KEY@default
###< symfony/sendgrid-mailer ###
###> mercure ###
# Internal hub URL used by the PHP app (reachable from the php container)
MERCURE_URL=http://mercure/.well-known/mercure
# Public hub URL used by browsers
MERCURE_PUBLIC_URL=https://mercure.escapepage.com/.well-known/mercure
MERCURE_PUBLIC_URL=http://localhost:8090/.well-known/mercure
# Shared secret for signing JWTs (dev only). In prod, set via real env/secrets.
MERCURE_JWT_SECRET=!ChangeThisMercureJWTSignedBySymfonySecretKey!
# Pre-generated JWT tokens for convenience (signed with the dev secret above)
MERCURE_PUBLISHER_JWT_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.E5b7ma4k-kA7lVGOQtICh7r2sspwX4G1iOhwtbxHQck
MERCURE_SUBSCRIBER_JWT_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIqIl19fQ.mwSAjvbm6vOnjMoRSHMdcqapNCwyGZs1s57uLK4T3UM
# CORS allowed origins (default)
MERCURE_CORS_ALLOWED_ORIGINS="https://www.escapepage.com https://escapepage.com"
# Base URL for Mercure topics.
MERCURE_TOPIC_BASE=https://escapepage.com
MERCURE_JWT_SECRET=!ChangeThisMercureJWT!
# Base URL for Mercure topics. Use .dev in development; override to .com in prod via .env.prod or real env.
MERCURE_TOPIC_BASE=https://escapepage.dev
###< mercure ###
###> docker ###
USER_ID=1000
GROUP_ID=1000
###< docker ###
###> karser/karser-recaptcha3-bundle ###
# Get your API key and secret from https://g.co/recaptcha/v3
RECAPTCHA3_KEY=CHANGEME_RECAPTCHA3_KEY
RECAPTCHA3_SECRET=CHANGEME_RECAPTCHA3_SECRET
###< karser/karser-recaptcha3-bundle ###
+4
View File
@@ -0,0 +1,4 @@
###> symfony/framework-bundle ###
APP_SECRET=620e9ce5f88a714b636179eb39d5be4f
###< symfony/framework-bundle ###
+9
View File
@@ -0,0 +1,9 @@
### Compiled or real environment variables should be used in production.
### Configure MAILER_DSN to use SendGrid API transport.
### Prefer storing SENDGRID_API_KEY using Symfony Secrets or real env vars.
###> symfony/mailer ###
# Example using SendGrid API key (replace with real secret via vault/secrets):
# SENDGRID_API_KEY=SG.xxxxx
MAILER_DSN=sendgrid+api://%env(resolve:SENDGRID_API_KEY)%@default
###< symfony/mailer ###
+3
View File
@@ -0,0 +1,3 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
-13
View File
@@ -1,18 +1,11 @@
###> symfony/framework-bundle ###
/.env
/.env.dev
/.env.prod
/.env.test
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
!/var/volumes/
/var/volumes/*
!/var/volumes/.gitignore
/vendor/
###< symfony/framework-bundle ###
@@ -32,9 +25,3 @@
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###
/.idea
###> docker env ###
/docker/.env
###< docker env ###
-1
View File
@@ -140,7 +140,6 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/webpack-encore-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/reset-password-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/verify-email-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/karser/karser-recaptcha3-bundle" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
Generated
+3 -4
View File
@@ -12,7 +12,7 @@
</component>
<component name="PhpCodeSniffer">
<phpcs_settings>
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="5505d524-8d4c-4fe7-a2cb-82e334156ed6" timeout="30000" />
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="94dffba0-1483-4719-8129-fde9aa527533" timeout="30000" />
</phpcs_settings>
</component>
<component name="PhpIncludePathManager">
@@ -153,13 +153,12 @@
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
<path value="$PROJECT_DIR$/vendor/symfonycasts/verify-email-bundle" />
<path value="$PROJECT_DIR$/vendor/symfonycasts/reset-password-bundle" />
<path value="$PROJECT_DIR$/vendor/karser/karser-recaptcha3-bundle" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
<component name="PhpStan">
<PhpStan_settings>
<phpstan_by_interpreter asDefaultInterpreter="true" interpreter_id="5505d524-8d4c-4fe7-a2cb-82e334156ed6" timeout="60000" />
<phpstan_by_interpreter asDefaultInterpreter="true" interpreter_id="94dffba0-1483-4719-8129-fde9aa527533" timeout="60000" />
</PhpStan_settings>
</component>
<component name="PhpStanOptionsConfiguration">
@@ -172,7 +171,7 @@
</component>
<component name="Psalm">
<Psalm_settings>
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="5505d524-8d4c-4fe7-a2cb-82e334156ed6" timeout="60000" />
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="94dffba0-1483-4719-8129-fde9aa527533" timeout="60000" />
</Psalm_settings>
</component>
<component name="PsalmOptionsConfiguration">
+17 -13
View File
@@ -16,7 +16,7 @@ This repository contains a Symfony 7.3 (PHP >= 8.5.1) application for a collabor
6. Run tests: `vendor/bin/phpunit`
- With Docker:
1. `cd docker && docker compose up -d`
1. From `docker/`: `docker compose up -d`
2. Install vendors inside the PHP container:
- `docker compose exec php bash`
- `composer install`
@@ -25,25 +25,23 @@ This repository contains a Symfony 7.3 (PHP >= 8.5.1) application for a collabor
- `php bin/console doctrine:migrations:migrate -n`
4. App is at http://localhost:8080
## Email (Mailpit in dev, Mailgun for prod)
## Email (Mailpit in dev, SendGrid for prod)
- Dev: a `mailer` service (Mailpit) runs in Docker.
- SMTP DSN in `.env`: `MAILER_DSN=smtp://mailer:1025`
- Mailpit UI: http://localhost:8025 (or mapped port 8025)
- Mailpit UI: http://localhost:8025
- Send a test mail: `php bin/console app:mail:test you@example.com`
- Staging/Prod: use Mailgun.
- Require package (already in composer): `symfony/mailgun-mailer`.
- Staging/Prod: use SendGrid.
- Require package (already in composer): `symfony/sendgrid-mailer`.
- Set environment variables (do NOT commit secrets):
- `MAILER_DSN=mailgun+api://${MAILGUN_API_KEY}:${MAILGUN_DOMAIN}@default?region=eu`
- `MAILGUN_API_KEY=YOUR_REAL_KEY`
- `MAILGUN_DOMAIN=YOUR_SENDING_DOMAIN` (e.g. `mg.escapepage.nl`)
- `MAILER_DSN=sendgrid+api://%env(SENDGRID_API_KEY)%`
- `SENDGRID_API_KEY=YOUR_REAL_KEY`
- Optional: `MAILER_FROM=no-reply@your-domain.tld`
- Drop `region=eu` (or use `region=us`) depending on which region your Mailgun domain was created in.
- Alternatively via SMTP (no extra package):
- `MAILER_DSN="mailgun+smtp://USERNAME:PASSWORD@default?region=eu"`
- `MAILER_DSN="smtp://apikey:%env(SENDGRID_API_KEY)%@smtp.sendgrid.net:587?encryption=tls"`
Troubleshooting:
- If emails dont appear in dev, open Mailpit at http://localhost:8025 and verify messages.
- In prod, check logs for HTTP 2xx responses from Mailgun and verify sender domain is verified (SPF/DKIM) in Mailgun.
- In prod, check logs for HTTP 2xx responses from SendGrid and verify sender domain is verified in SendGrid.
## Frontend assets with Webpack Encore
We use Webpack Encore to build and minify JS/CSS from the `assets/` directory into `public/build/`.
@@ -83,9 +81,9 @@ See doc/CONTRIBUTING.md for code style and more details.
We use a Mercure hub (Docker service) to push server updates to browsers via ServerSent Events (SSE).
Quick start (dev):
1. Start Docker stack:
1. Start Docker stack from `docker/`:
```
cd docker && docker compose up -d
docker compose up -d
```
This starts `mercure` at http://localhost:8090 and the app at http://localhost:8080.
2. Install PHP deps inside the PHP container if you haven't yet:
@@ -93,6 +91,12 @@ Quick start (dev):
docker compose exec php bash
composer install
```
3. Open the Game Hub page in your browser: http://localhost:8080/game
- The page subscribes to a demo topic and logs messages in the console.
4. Publish a test update (in the PHP container):
```
php bin/console app:mercure:publish
```
You should see a console log like `[Mercure] Update received: { ... }` on the Game Hub page.
Configuration:
-27
View File
@@ -1,27 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
const buttons = document.querySelectorAll('[data-tab-target]');
if (!buttons.length) {
return;
}
const activate = (id) => {
document.querySelectorAll('.admin-tab-panel').forEach(el => el.style.display = 'none');
const target = document.getElementById(id);
if (target) {
target.style.display = 'block';
}
buttons.forEach(btn => {
const active = btn.dataset.tabTarget === id;
btn.style.background = active ? '#fff' : '#f8fafc';
btn.style.color = active ? '#1e40af' : '#64748b';
btn.style.fontWeight = active ? '600' : '400';
btn.style.borderColor = active ? '#3b82f6' : '#e2e8f0';
btn.style.borderBottom = active ? '1px solid #fff' : '1px solid #e2e8f0';
});
};
buttons.forEach(btn => {
btn.addEventListener('click', () => activate(btn.dataset.tabTarget));
});
});
+3 -7
View File
@@ -1,10 +1,6 @@
/*
* Welcome to your app's main JavaScript file!
*/
import './styles/app.scss';
import 'bootstrap/js/dist/collapse';
import 'bootstrap/js/dist/alert';
import 'bootstrap/js/dist/dropdown';
import './game-waiting';
import './game-lobby';
import './admin-session-tabs';
import './styles/app.css';
console.log('This log comes from assets/app.js built by Webpack Encore! 🎉');
-62
View File
@@ -1,62 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
const config = document.getElementById('game-lobby-config');
if (!config) {
return;
}
const publicUrl = config.dataset.mercurePublicUrl;
const topic = config.dataset.topic;
const chatLog = document.getElementById('lobby-chat-log');
function appendLobbyMessage(username, content, createdAt) {
if (!chatLog) {
return;
}
const emptyNotice = document.getElementById('lobby-chat-empty');
if (emptyNotice) {
emptyNotice.remove();
}
const time = createdAt
? new Date(createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
: '';
const wrapper = document.createElement('div');
wrapper.className = 'lobby-message';
const author = document.createElement('strong');
author.textContent = username;
const timestamp = document.createElement('span');
timestamp.className = 'text-muted small';
timestamp.textContent = ' ' + time;
const body = document.createElement('div');
body.textContent = content;
wrapper.appendChild(author);
wrapper.appendChild(timestamp);
wrapper.appendChild(body);
chatLog.appendChild(wrapper);
chatLog.scrollTop = chatLog.scrollHeight;
}
if (publicUrl && topic) {
const url = new URL(publicUrl);
url.searchParams.append('topic', topic);
const eventSource = new EventSource(url);
eventSource.onmessage = event => {
const data = JSON.parse(event.data);
if (data.type === 'lobby_message') {
appendLobbyMessage(data.username, data.content, data.createdAt);
} else if (data.type === 'player_joined' || data.type === 'session_started') {
window.location.reload();
}
};
}
if (chatLog) {
chatLog.scrollTop = chatLog.scrollHeight;
}
});
-68
View File
@@ -1,68 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
const config = document.getElementById('game-waiting-config');
if (!config) {
return;
}
const publicUrl = config.dataset.mercurePublicUrl;
const topic = config.dataset.topic;
const readyAt = config.dataset.readyAt;
let reloading = false;
const reloadOnce = (eventSource) => {
if (reloading) {
return;
}
reloading = true;
if (eventSource) {
eventSource.close();
}
window.location.reload();
};
if (publicUrl && topic) {
const url = new URL(publicUrl);
url.searchParams.append('topic', topic);
const eventSource = new EventSource(url);
eventSource.onmessage = event => {
const data = JSON.parse(event.data);
if (data.type === 'all_ready' || data.type === 'player_ready') {
reloadOnce(eventSource);
}
};
}
// Our own ready status expires 60s after we set it - proactively tell the
// server as close to that deadline as possible, so the other players find
// out live instead of only whenever someone else's request happens to
// trigger the lazy check.
if (readyAt) {
const timeoutMs = 61000; // slightly more than the server-side 60s
const readyAtMs = readyAt * 1000;
const countdownEl = document.getElementById('ready-countdown');
const expireForm = document.getElementById('expire-ready-form');
const updateCountdown = () => {
const remaining = Math.max(0, Math.ceil((readyAtMs + timeoutMs - Date.now()) / 1000));
if (countdownEl) {
const m = Math.floor(remaining / 60);
const s = remaining % 60;
countdownEl.textContent = m + ':' + s.toString().padStart(2, '0');
}
return remaining;
};
const remaining = updateCountdown();
if (remaining <= 0) {
expireForm?.submit();
} else {
const countdownInterval = setInterval(() => {
if (updateCountdown() <= 0) {
clearInterval(countdownInterval);
expireForm?.submit();
}
}, 1000);
}
}
});
+4 -393
View File
@@ -1,16 +1,7 @@
/* Game1 entry point built with Webpack Encore */
import './styles/game1.css';
let sequenceFinished = false;
let stillPlayingSound = true;
let navigatingAway = false;
function goTo(url) {
navigatingAway = true;
window.location.href = url;
}
function subscribeToMercure(mercurePublicUrl, topic, myScreen, wonUrl, lostUrl) {
function subscribeToMercure(mercurePublicUrl, topic) {
try {
const url = mercurePublicUrl + '?topic=' + encodeURIComponent(topic);
const es = new EventSource(url);
@@ -19,36 +10,6 @@ function subscribeToMercure(mercurePublicUrl, topic, myScreen, wonUrl, lostUrl)
try {
const data = JSON.parse(event.data);
console.log('[Mercure][game1] Update:', data);
if (data && !Array.isArray(data) && data.type === 'game_finished') {
const destination = data.status === 'won' ? wonUrl : lostUrl;
if (destination) {
goTo(destination);
}
return;
}
// data is [sendTo, message, messageType?] - messageType defaults to 'mainframe' (green)
if (Array.isArray(data) && data.length >= 2) {
const sendTo = parseInt(data[0]);
// Filter: 0 means everyone, otherwise must match myScreen
if (sendTo !== 0 && sendTo !== parseInt(myScreen)) {
console.log('[Mercure][game1] Message not for this player, skipping.');
return;
}
const messageContainer = document.getElementById('message-container');
if (messageContainer) {
appendResultMessage(messageContainer, data[1], data[2] || 'mainframe');
window.scrollTo(0, document.body.scrollHeight);
if(stillPlayingSound)
playSound();
console.log('[Mercure][game1] sequenceFinished status:', sequenceFinished);
if (sequenceFinished) {
flashRed();
}
}
}
} catch (e) {
console.log('[Mercure][game1] Raw event:', event.data);
}
@@ -64,170 +25,6 @@ function subscribeToMercure(mercurePublicUrl, topic, myScreen, wonUrl, lostUrl)
}
}
function playSound() {
const sound = document.getElementById('message-sound');
if (sound) {
sound.currentTime = 0;
sound.play().catch(e => console.warn('[Audio] Playback failed:', e));
}
}
function flashRed() {
console.log('[Game1] Triggering flashRed');
const body = document.body;
body.classList.remove('flash-red');
void body.offsetWidth; // Trigger reflow to restart animation
body.classList.add('flash-red');
// Also remove it after animation finishes so it's clean for inspection
setTimeout(() => {
body.classList.remove('flash-red');
console.log('[Game1] Removed flash-red class');
}, 150);
}
let lockRevealTimer = null;
let lockExpireTimer = null;
let lockCountdownTimer = null;
let currentLockedAt = null;
function lockMessageClass(messageType) {
if (messageType === 'virus') return 'message-virus';
if (messageType === 'mainframe') return 'message-mainframe';
if (messageType === 'hint') return 'message-hint';
return '';
}
function appendResultMessage(container, text, messageType) {
const msgEl = document.createElement('div');
msgEl.className = ('message ' + lockMessageClass(messageType)).trim();
msgEl.textContent = text;
container.appendChild(msgEl);
}
function setInputDisabled(disabled) {
const inputField = document.getElementById('input-message');
if (inputField) {
inputField.disabled = disabled;
}
}
function clearLockTimers() {
if (lockRevealTimer) { clearTimeout(lockRevealTimer); lockRevealTimer = null; }
if (lockExpireTimer) { clearTimeout(lockExpireTimer); lockExpireTimer = null; }
if (lockCountdownTimer) { clearInterval(lockCountdownTimer); lockCountdownTimer = null; }
}
function clearLock() {
clearLockTimers();
currentLockedAt = null;
document.body.classList.remove('locked');
const banner = document.getElementById('lock-banner');
if (banner) banner.style.display = 'none';
setInputDisabled(false);
}
function updateLockCountdown(unlockAtMs) {
const countdownEl = document.getElementById('lock-countdown');
if (!countdownEl) return;
const remaining = Math.max(0, Math.ceil((unlockAtMs - Date.now()) / 1000));
countdownEl.textContent = remaining + 's';
}
async function fetchLockReveal(apiEchoUrl, messageContainer) {
if (!apiEchoUrl) return;
try {
const response = await fetchJson(apiEchoUrl, {
method: 'POST',
body: { message: '', ts: new Date().toISOString() },
});
const result = response && response.result;
if (result && Array.isArray(result.result)) {
result.result.forEach(text => appendResultMessage(messageContainer, text, result.messageType));
window.scrollTo(0, document.body.scrollHeight);
}
if (result && result.locked === false) {
clearLock();
return;
}
// Code has been revealed (or already was), let the player try /unlock
setInputDisabled(false);
} catch (e) {
console.error('[Game1] Failed to fetch lock reveal:', e);
}
}
function applyLock(lockData, apiEchoUrl, messageContainer) {
if (currentLockedAt === lockData.lockedAt) {
return; // already tracking this lock, avoid re-fetching/duplicating messages
}
currentLockedAt = lockData.lockedAt;
clearLockTimers();
const banner = document.getElementById('lock-banner');
if (banner) banner.style.display = 'flex';
document.body.classList.add('locked');
const revealAtMs = lockData.revealAt * 1000;
const unlockAtMs = lockData.unlockAt * 1000;
const now = Date.now();
if (now < revealAtMs) {
setInputDisabled(true);
lockRevealTimer = setTimeout(() => fetchLockReveal(apiEchoUrl, messageContainer), revealAtMs - now);
} else {
fetchLockReveal(apiEchoUrl, messageContainer);
}
lockExpireTimer = setTimeout(() => clearLock(), Math.max(0, unlockAtMs - now));
updateLockCountdown(unlockAtMs);
lockCountdownTimer = setInterval(() => {
updateLockCountdown(unlockAtMs);
if (Date.now() >= unlockAtMs) {
clearInterval(lockCountdownTimer);
lockCountdownTimer = null;
}
}, 1000);
}
let filesRemovalTimer = null;
let scheduledFilesRemovalDeadline = null;
async function pingFilesRemovalDeadline(apiEchoUrl) {
if (!apiEchoUrl) return;
try {
// A no-op message is enough to make the server evaluate the deadline server-side;
// the actual restore notice (if any) arrives for everyone via the Mercure broadcast.
await fetchJson(apiEchoUrl, {
method: 'POST',
body: { message: '', ts: new Date().toISOString() },
});
} catch (e) {
console.error('[Game1] Failed to ping files-removal deadline:', e);
}
}
function scheduleFilesRemovalCheck(deadline, apiEchoUrl) {
if (!deadline || scheduledFilesRemovalDeadline === deadline) {
return; // nothing to (re)schedule
}
scheduledFilesRemovalDeadline = deadline;
if (filesRemovalTimer) {
clearTimeout(filesRemovalTimer);
filesRemovalTimer = null;
}
const delay = Math.max(0, deadline * 1000 - Date.now());
filesRemovalTimer = setTimeout(() => {
filesRemovalTimer = null;
scheduledFilesRemovalDeadline = null;
pingFilesRemovalDeadline(apiEchoUrl);
}, delay);
}
async function fetchJson(url, options = {}) {
const opts = { ...options };
const headers = new Headers(opts.headers || {});
@@ -263,18 +60,6 @@ document.addEventListener('DOMContentLoaded', async () => {
// Look for config injected by Twig in the page
const cfgEl = document.getElementById('mercure-config');
// Prevent/warn on page reload, except for our own win/lose redirects
window.addEventListener('beforeunload', (event) => {
if (navigatingAway) {
return;
}
// Standard way to trigger the browser's confirmation dialog
event.preventDefault();
// Included for compatibility with older browsers
event.returnValue = '';
});
if (!cfgEl) {
console.warn('[Mercure][game1] #mercure-config element not found on page');
return;
@@ -282,74 +67,16 @@ document.addEventListener('DOMContentLoaded', async () => {
const mercurePublicUrl = cfgEl.dataset.mercurePublicUrl;
const topic = cfgEl.dataset.topic;
const screen = cfgEl.dataset.screen;
const apiPingUrl = cfgEl.dataset.apiPingUrl;
const apiEchoUrl = cfgEl.dataset.apiEchoUrl;
const apiCheckFinishedUrl = cfgEl.dataset.apiCheckFinishedUrl;
const lostUrl = cfgEl.dataset.lostUrl;
const wonUrl = cfgEl.dataset.wonUrl;
const lockLockedAt = cfgEl.dataset.lockLockedAt;
const lockRevealAt = cfgEl.dataset.lockRevealAt;
const lockUnlockAt = cfgEl.dataset.lockUnlockAt;
const filesRemovalDeadline = cfgEl.dataset.filesRemovalDeadline;
// Resume the auto-restore timer after a page refresh, if a window is already running
if (filesRemovalDeadline) {
scheduleFilesRemovalCheck(parseInt(filesRemovalDeadline, 10), apiEchoUrl);
}
const userId = cfgEl.dataset.userId;
if (mercurePublicUrl && topic) {
subscribeToMercure(mercurePublicUrl, topic, screen, wonUrl, lostUrl);
subscribeToMercure(mercurePublicUrl, topic);
} else {
console.warn('[Mercure][game1] Missing data attributes on #mercure-config');
}
// Timer logic
const timerEl = document.getElementById('game-timer');
if (timerEl && timerEl.dataset.endTime) {
const endTime = parseInt(timerEl.dataset.endTime) * 1000;
const updateTimer = async () => {
const now = Date.now();
const diff = endTime - now;
if (diff <= 0) {
timerEl.textContent = '00:00:00';
// Timer reached zero, check with server
if (apiCheckFinishedUrl && lostUrl) {
try {
const response = await fetchJson(apiCheckFinishedUrl, { method: 'POST' });
if (response && response.finished) {
goTo(response.status === 'won' && wonUrl ? wonUrl : lostUrl);
return; // Stop the timer loop
}
} catch (e) {
console.error('[API][game1] Failed to check finished status:', e);
}
}
// Even if check failed or not finished, stop the loop if diff <= 0
// (though technically if the server says not finished, we might want to keep checking,
// but 00:00:00 usually means it's over).
return;
}
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
const hDisplay = hours.toString().padStart(2, '0');
const mDisplay = minutes.toString().padStart(2, '0');
const sDisplay = seconds.toString().padStart(2, '0');
timerEl.textContent = `${hDisplay}:${mDisplay}:${sDisplay}`;
setTimeout(updateTimer, 1000);
};
updateTimer();
}
// Demo API calls
try {
if (apiPingUrl) {
@@ -362,7 +89,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (apiEchoUrl) {
const echo = await fetchJson(apiEchoUrl, {
method: 'POST',
body: { message: 'from game1.js', ts: new Date().toISOString() },
body: { message: 'from game1.js', ts: new Date().toISOString(), user: userId },
});
console.log('[API][game1] echo →', echo);
} else {
@@ -371,120 +98,4 @@ document.addEventListener('DOMContentLoaded', async () => {
} catch (e) {
console.error('[API][game1] Request failed:', e);
}
// Add messages to message-container
const messageContainer = document.getElementById('message-container');
if (messageContainer) {
let messages = [
['System initializing...', 500],
['Connection established.', 200],
['Welcome agent to the mainframe.', 1000],
['Scanning...', 3000],
['Virus detected.', 500],
['Starting Mainframe help modus...', 2000],
['Help modus activated.', 500],
['Blocking virus activated', 0]
];
let currentMessageIndex = 0;
const printNextMessage = () => {
if (currentMessageIndex < messages.length) {
const msg = messages[currentMessageIndex];
const msgEl = document.createElement('div');
let extraClass = '';
if(msg[2])
extraClass = msg[2];
msgEl.className = 'message ' + extraClass;
msgEl.textContent = msg[0];
messageContainer.appendChild(msgEl);
window.scrollTo(0, document.body.scrollHeight);
playSound();
currentMessageIndex++;
setTimeout(printNextMessage, msg[1]);
if (sequenceFinished) {
flashRed();
}
} else {
// After it has printed a set of messages, it has to start a timer of 2 seconds
console.log('[Game1] All messages printed. Starting 2s timer to expand message-container height...');
setTimeout(() => {
messageContainer.style.height = '400vh';
const inputField = document.getElementById('input-message');
inputField.disabled = false;
// Add event listener for Enter key
inputField.addEventListener('keypress', async (e) => {
if (e.key === 'Enter') {
stillPlayingSound = false;
sequenceFinished = false;
const message = inputField.value.trim();
const msgEl = document.createElement('div');
msgEl.className = 'message';
msgEl.textContent = message;
messageContainer.appendChild(msgEl);
if (message && apiEchoUrl) {
inputField.value = '';
try {
const response = await fetchJson(apiEchoUrl, {
method: 'POST',
body: { message, ts: new Date().toISOString() },
});
console.log('[API][game1] message sent →', response);
if (response && response.result && Array.isArray(response.result.result)) {
response.result.result.forEach(text => appendResultMessage(messageContainer, text, response.result.messageType));
window.scrollTo(0, document.body.scrollHeight);
}
if (response && response.result) {
if (response.result.gameWon === true && wonUrl) {
goTo(wonUrl);
return;
}
if (response.result.locked === true) {
applyLock({
lockedAt: response.result.lockedAt,
revealAt: response.result.revealAt,
unlockAt: response.result.unlockAt,
}, apiEchoUrl, messageContainer);
} else if (response.result.locked === false) {
clearLock();
}
if (response.result.filesRemovalDeadline) {
scheduleFilesRemovalCheck(response.result.filesRemovalDeadline, apiEchoUrl);
}
}
} catch (err) {
console.error('[API][game1] Failed to send message:', err);
}
}
}
});
console.log('[Game1] message-container height changed to 400vh and input enabled');
sequenceFinished = true;
console.log('[Game1] sequenceFinished is now TRUE');
// Restore an in-progress lock after a page refresh
if (lockUnlockAt && parseInt(lockUnlockAt, 10) * 1000 > Date.now()) {
applyLock({
lockedAt: parseInt(lockLockedAt, 10),
revealAt: parseInt(lockRevealAt, 10),
unlockAt: parseInt(lockUnlockAt, 10),
}, apiEchoUrl, messageContainer);
}
}, 2000);
}
};
printNextMessage();
}
});
@@ -1,8 +0,0 @@
ServerRoot "/etc/apache2"
Listen 80
User www-data
Group www-data
ErrorLog ${APACHE_LOG_DIR}/error.log
LogLevel warn
IncludeOptional mods-enabled/*.load
IncludeOptional sites-enabled/*.conf
@@ -1,3 +0,0 @@
deb http://deb.debian.org/debian bookworm main contrib non-free-firmware
deb http://deb.debian.org/debian bookworm-updates main contrib non-free-firmware
deb http://security.debian.org/debian-security bookworm-security main contrib non-free-firmware
-8
View File
@@ -1,8 +0,0 @@
# /etc/crontab: system-wide crontab
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || run-parts --report /etc/cron.daily
47 6 * * 7 root test -x /usr/sbin/anacron || run-parts --report /etc/cron.weekly
52 6 1 * * root test -x /usr/sbin/anacron || run-parts --report /etc/cron.monthly
-1
View File
@@ -1 +0,0 @@
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
-4
View File
@@ -1,4 +0,0 @@
# /etc/fstab: static file system information.
UUID=8f14e45f-ceea-4a63-9b3f-1a2b3c4d5e6f / ext4 errors=remount-ro 0 1
UUID=1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d /boot ext4 defaults 0 2
/swapfile none swap sw 0 0
-1
View File
@@ -1 +0,0 @@
archive-node-04
-6
View File
@@ -1,6 +0,0 @@
127.0.0.1 localhost
127.0.1.1 archive-node-04
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.0.0.4 archive-node-04.internal
-2
View File
@@ -1,2 +0,0 @@
Debian GNU/Linux 12 \n \l
-2
View File
@@ -1,2 +0,0 @@
Welcome to archive-node-04.
All connections are logged and monitored for internal review purposes.
@@ -1,10 +0,0 @@
source /etc/network/interfaces.d/*
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
address 10.0.0.4
netmask 255.255.255.0
gateway 10.0.0.1
@@ -1,14 +0,0 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 768;
}
http {
sendfile on;
keepalive_timeout 65;
include /etc/nginx/mime.types;
include /etc/nginx/sites-enabled/*;
}
@@ -1,9 +0,0 @@
passwd: files
group: files
shadow: files
hosts: files dns
networks: files
protocols: db files
services: db files
ethers: db files
rpc: db files
-8
View File
@@ -1,8 +0,0 @@
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
-11
View File
@@ -1,11 +0,0 @@
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
admin:x:1000:1000:admin,,,:/home/admin:/bin/bash
guest:x:1001:1001:guest,,,:/home/guest:/bin/bash
-3
View File
@@ -1,3 +0,0 @@
nameserver 1.1.1.1
nameserver 9.9.9.9
options edns0
-7
View File
@@ -1,7 +0,0 @@
root:$6$rounds=656000$xJ2kLQmZ$aFq9zN3vQwErTyUiOpAsDfGhJkLzXcVbNm1234567890abcdefgh:19700:0:99999:7:::
daemon:*:19700:0:99999:7:::
bin:*:19700:0:99999:7:::
sys:*:19700:0:99999:7:::
sshd:*:19700:0:99999:7:::
admin:$6$rounds=656000$k3PqR8tW$bGr0oPqLmNbVcXzAsDfGhJkLqWeRtYuIoP0987654321zyxwvu:19700:0:99999:7:::
guest:*:19700:0:99999:7:::
@@ -1,4 +0,0 @@
Host *
SendEnv LANG LC_*
HashKnownHosts yes
GSSAPIAuthentication yes
@@ -1,7 +0,0 @@
Port 22
PermitRootLogin no
PasswordAuthentication yes
PubkeyAuthentication yes
X11Forwarding no
PrintMotd no
Subsystem sftp /usr/lib/openssh/sftp-server
-1
View File
@@ -1 +0,0 @@
Europe/Amsterdam
@@ -1,8 +0,0 @@
app:
name: internal-archive-sync
version: 2.3.1
log_level: info
port: 8080
database:
driver: sqlite
path: /opt/app/data.db
@@ -1,5 +0,0 @@
apt update
apt upgrade -y
systemctl restart nginx
df -h
journalctl -xe
-10
View File
@@ -1,10 +0,0 @@
# ~/.bashrc: executed by bash for non-login shells
case $- in
*i*) ;;
*) return;;
esac
export PS1='\u@\h:\w\$ '
alias ll='ls -alF'
alias la='ls -A'
@@ -1 +0,0 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGZ8pQxT2mN0vRkLwYb6cJhU3sEoAeKdVmXpZq7tRnBs admin@archive-node-04
@@ -1,2 +0,0 @@
update-alternatives 2026-05-30 03:10:04: link group editor updated to point to /usr/bin/vim.basic
update-alternatives 2026-05-30 03:10:04: link group pager updated to point to /usr/bin/less
-6
View File
@@ -1,6 +0,0 @@
Jun 12 09:41:55 archive-node-04 sshd[10233]: Accepted publickey for admin from 10.0.0.7 port 51422 ssh2
Jun 12 09:41:55 archive-node-04 sshd[10233]: pam_unix(sshd:session): session opened for user admin by (uid=0)
Jun 12 09:55:02 archive-node-04 sudo: admin : TTY=pts/0 ; PWD=/home/admin ; USER=root ; COMMAND=/usr/bin/apt update
Jun 12 10:12:40 archive-node-04 sshd[10233]: pam_unix(sshd:session): session closed for user admin
Jun 12 22:03:11 archive-node-04 sshd[15092]: Failed password for invalid user test from 203.0.113.44 port 39102 ssh2
Jun 12 22:03:14 archive-node-04 sshd[15092]: Connection closed by 203.0.113.44 port 39102 [preauth]
-4
View File
@@ -1,4 +0,0 @@
[ OK ] Started Network Manager.
[ OK ] Started OpenSSH server daemon.
[ OK ] Started Nginx HTTP server.
[ OK ] Reached target Multi-User System.
-2
View File
@@ -1,2 +0,0 @@
Jun 12 04:00:11 archive-node-04 CRON[9021]: (root) CMD (test -x /usr/sbin/anacron || run-parts --report /etc/cron.daily)
Jun 12 18:30:02 archive-node-04 CRON[15544]: (root) CMD (test -x /usr/sbin/anacron || run-parts --report /etc/cron.hourly)
@@ -1,2 +0,0 @@
Jun 12 03:00:05 archive-node-04 systemd-udevd[512]: Using default interface naming scheme 'v252'.
Jun 12 03:00:11 archive-node-04 dbus-daemon[601]: [system] Successfully activated service 'org.freedesktop.hostname1'
-4
View File
@@ -1,4 +0,0 @@
[ 0.000000] Linux version 6.1.0-21-amd64 (debian-kernel@lists.debian.org)
[ 0.004211] Command line: BOOT_IMAGE=/boot/vmlinuz-6.1.0-21-amd64 root=UUID=8f14e45f
[ 0.512033] ACPI: Core revision 20221020
[ 1.221004] usb 1-1: new high-speed USB device number 2
-4
View File
@@ -1,4 +0,0 @@
2026-05-30 03:10:02 startup archives unpack
2026-05-30 03:10:04 install curl:amd64 <none> 8.4.0-2
2026-05-30 03:10:05 status installed curl:amd64 8.4.0-2
2026-06-02 09:44:11 upgrade openssh-server:amd64 1:9.2p1-2 1:9.2p1-2+deb12u2
-4
View File
@@ -1,4 +0,0 @@
Jun 12 03:00:02 archive-node-04 kernel: [ 0.000000] Linux version 6.1.0-21-amd64
Jun 12 03:00:02 archive-node-04 kernel: [ 0.004211] Command line: BOOT_IMAGE=/boot/vmlinuz-6.1.0-21-amd64 root=UUID=8f14e45f
Jun 12 03:00:03 archive-node-04 kernel: [ 1.221004] usb 1-1: new high-speed USB device number 2
Jun 12 03:00:03 archive-node-04 kernel: [ 1.552210] eth0: link up, 1000Mbps, full-duplex
-2
View File
@@ -1,2 +0,0 @@
Jun 12 05:11:02 archive-node-04 postfix/qmgr[812]: 3F2A1C0021: removed
Jun 12 05:11:02 archive-node-04 postfix/smtp[9944]: 3F2A1C0021: to=<root@localhost>, status=sent
-8
View File
@@ -1,8 +0,0 @@
Jun 12 03:12:01 archive-node-04 systemd[1]: Starting Daily apt download activities...
Jun 12 03:12:04 archive-node-04 systemd[1]: apt-daily.service: Deactivated successfully.
Jun 12 04:00:11 archive-node-04 CRON[9021]: (root) CMD (test -x /usr/sbin/anacron || run-parts --report /etc/cron.daily)
Jun 12 06:25:00 archive-node-04 anacron[1122]: Job `cron.daily' terminated
Jun 12 09:41:55 archive-node-04 sshd[10233]: Accepted publickey for admin from 10.0.0.7 port 51422 ssh2
Jun 12 09:41:55 archive-node-04 sshd[10233]: pam_unix(sshd:session): session opened for user admin
Jun 12 12:03:19 archive-node-04 systemd[1]: Reloading nginx.service
Jun 12 18:30:02 archive-node-04 CRON[15544]: (root) CMD (test -x /usr/sbin/anacron || run-parts --report /etc/cron.hourly)
-4
View File
@@ -1,4 +0,0 @@
From cron@archive-node-04 Wed Jun 10 06:25:01 2026
Subject: Cron <root@archive-node-04> run-parts --report /etc/cron.daily
Daily housekeeping completed without errors.
@@ -1,10 +0,0 @@
Undercover Report Case File 145-93
Agent: Carver (cover identity)
Date: 02/18 02/22
Subject operates a rural safehouse rumored to shelter fugitives and serve as a transient waypoint. Agents cover as a drifter seeking work provided entry in exchange for chores. On 02/19, visitors with gang tattoos arrived after midnight and were housed in separate rooms. Vehicles were parked under tree cover, and license plates were mudsmeared. On 02/20, the subject warned of “new heat in town” and began burning scrap papers in a barrel.
Later that night, a duffel of documents was left unattended. The agent briefly photographed maps with marked backroad routes and coded notes. The handwriting matches samples from a related case. On 02/21, the subject probed the agents story with pointed questions about prior arrests. Cover held after the agent recited the rehearsed backstory.
Cover credible but timelimited. Recommend immediate analysis of the photographed maps, ID of transient visitors via tattoos and vehicle features, and a synchronized warrant service before the location rotates to a fresh site.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,11 +0,0 @@
Undercover Report Case File 19-52
Agent: Harper (cover identity)
Date: 07/22 07/26
Subject controls a pawnshop that exhibits transaction volumes and cash handling inconsistent with normal retail activity. Cover identity as electronics repair tech granted backroom access to the testing bench, DVR cabinet, and inventory cage. On 07/23, the subject hosted a closeddoor meeting with four associates who arrived separately and staggered departures. Through the vent grate, agent heard references to “offshore wires,” “clean accounts,” and “quarterend flush.” Subject maintained a spiral ledger with denominations recorded in columns that do not match typical pointofsale exports.
On 07/24, the subject ordered cameras repositioned to avoid capturing the safe directly. Agent was subtly tested regarding loyalty—assigned an afterhours soldering task while the subject counted bundled currency on the shop floor. Later that night, an unmarked van delivered two duffel bags that were moved to the vault without intake paperwork. Subject personally signed a receipt on blank stationery and pocketed the copy.
On 07/25, subject complained about “Treasury heat” and instructed the bookkeeper to route payments through two new accounts, both named with common surnames. Names did not match any employee records. Agent retrieved partial account numbers from a discarded note.
Cover remains credible. Recommend immediate financial taskforce review of the ledger, subpoena of the newly referenced accounts, and discreet licenseplate canvass for the van. Surveillance continuity is advised; subject exhibits rising paranoia yet continues to centralize cash at the location.
@@ -1,10 +0,0 @@
Undercover Report Case File 130-62
Agent: Lennox (cover identity)
Date: 12/02 12/06
Subject runs an art gallery suspected of laundering through inflated valuations and forged provenance. Agent embedded as gallery assistant. On 12/03, the subject finalized a cash sale for a painting at five times market value to an anonymous buyer. On 12/04, the subject ordered an assistant to “adjust provenance papers,” instructing edits to dates and prior ownership. The font choice and printer bleed matched a stack of older certificates in a locked drawer.
On 12/05, a private viewing hosted foreign clients who avoided staff contact. One carried a briefcase later exchanged for a wrapped canvas in the loading alcove. On 12/06, the subject cautioned staff not to speak with authorities and appeared tense while counting cash in the office.
Cover secure. Recommend seizure of the forged documents, liaison with culturalproperty experts to authenticate inventory, and identification of the foreign clients via travel manifests and nearby CCTV.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,12 +0,0 @@
Undercover Report Case File 94-31
Agent: Donovan (cover identity)
Date: 06/01 06/05
Subject operates a private marina suspected of facilitating smuggling. Agents cover as a yacht mechanic provides daily access to slips and latenight maintenance calls. On 06/02, the subject directed the agent to assist with unloading three sealed crates from a speedboat arriving past midnight. Crates bore generic “marine supplies” stickers; handlers treated them with unusual care. Opening was not feasible without tipping the crew.
On 06/03, a heated exchange with a subordinate revealed concerns over “late payments” from overseas. The subject warned that debts must be cleared “before customs comes sniffing.” The tone suggested reliance on offshore accounts. On 06/04, the subject introduced an associate known only as “Captain,” who exhibited military bearing and carried a sidearm; conversation centered on moving delivery windows to avoid routine patrol sweeps.
On 06/05, the subject carried a heavy duffel into the office and drew blinds for over an hour. Subsequent demeanor was relaxed, implying a successful cash reconciliation.
Cover intact. Recommend forensic review of marina accounting, financial tracing of suspected offshore links, and coordination with harbor patrol to flag vessels matching the speedboats profile.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,10 +0,0 @@
Undercover Report Case File 98-07
Agent: Fletcher (cover identity)
Date: 07/11 07/14
Subject serves as a lieutenant managing a distribution hub inside an abandoned textile mill. Cover identity as a shellcompany delivery driver grants entrance to the loading dock and elevator cages. On 07/12, the subject assigned the agent to ferry sealed packages to a storagelocker complex registered under a false name. Package weight and precautions suggest narcotics. Keys on a ring were photographed; several cuts matched lockers in Row C.
On 07/13, the subject referenced “Chicago buyers,” instructing the crew to increase output despite lawenforcement chatter. Morale appeared strained. On 07/14, the subject gathered the crew, delivered a loyalty speech about “traitors,” and scanned faces while the enforcer lingered by the door. No direct challenge to the agent, but suspicion is rising.
Cover holds but the environment is volatile. Recommend warrants for the locker complex keyed to the photographed cuts, discreet traffic stops on the distribution couriers, and a contingency extraction plan should violence erupt at the mill.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,10 +0,0 @@
Undercover Report Case File 123-88
Agent: Holloway (cover identity)
Date: 11/04 11/08
Subject leverages a regional trucking firm to move contraband under cover of legitimate freight. Agents cover as a dispatcher assistant enables access to GPS routes and driver notes. On 11/05, routing orders labeled “detour deliveries” consistently bypassed weigh stations, and manifests showed weight mismatches. On 11/06, the subject instructed a driver to deliver “straight to the mountain cabin,” providing coordinates later confirmed by GPS ping.
On 11/07, news of lawenforcement activity near the interstate caused the subject to cancel multiple runs. Crew anxiety rose. On 11/08, the subject praised the agent for rerouting a truck around a checkpoint, further legitimizing the cover.
Cover intact. Recommend aerial survey of the coordinates to identify storage, placement of covert trackers on the detour fleet, and financial review of the dispatch accounts to map the laundering flow.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,11 +0,0 @@
Undercover Report Case File 31-11
Agent: Vega (cover identity)
Date: 09/04 09/07
Subject runs a rural pharmacy with unusually high order volumes for specific controlled substances. Agents cover as a delivery technician enabled access to loading doors and packing slips. On 09/05, the subject accepted a pallet of blister packs outside normal receiving hours, directing it straight to a side room. Labels were legitimate, but lot numbers did not appear in the chainofcustody app used elsewhere in the store. Staff avoided eye contact and deferred to the subject for every deviation.
On 09/06, two sedan drivers arrived within minutes of each other, both collecting “returns” in sealed totes. Agent observed the subject bypassing the returns register and printing generic labels from a standalone thermal unit. Drivers refused signatures and left via the alley. The subject later shredded a stack of thermal drafts without balancing the counts.
On 09/07, agent overheard a phone call in which the subject referenced “doctor packs,” “holiday traffic,” and “quota pressure.” The cadence suggested coordination with a prescriber and an external distributor. The subjects mood shifted sharply after the call; staff were told to “keep it to cash only” for the remainder of the day.
Cover remains viable. Recommend a joint audit with state pharmacy regulators, review of the lot numbers observed, and controlled buys through the returns channel to document diversion.
@@ -1,12 +0,0 @@
Undercover Report Case File 53-28
Agent: Navarro (cover identity)
Date: 02/01 02/04
Subject owns a private courier service that advertises sameday delivery for tech firms. Shipment profiles and driver chatter suggest the operation moves highend electronics and cryptomining components off the books. Agents cover as a dispatch assistant provided map access and driver rotations. On 02/02, the subject created a route labeled “white run” that bypassed weigh stations and used alley handoffs. Driver later returned with a sealed envelope and no signature trail.
On 02/03, a pallet bearing unfamiliar consignee codes was loaded into a van with interior panels removed—consistent with contraband concealment. Subject lectured staff about “never opening the blue totes,” then carried one into a locked server closet. Agent heard a fan spinup and a brief data sync tone from a nearby laptop, implying a quickclone procedure.
On 02/04, the subject hinted the agent could “graduate to vest access” if weekend performance stayed quiet. A senior driver mentioned “hash boards” failing in transit and needing replacements before “the buyer from Reno flies out.”
Cover stable. Recommend covert GPS beacons on the whiterun vans, subpoena of the couriers API logs for the locked server closet, and coordination with aircargo teams for the Reno timeline.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,10 +0,0 @@
Undercover Report Case File 117-56
Agent: OConnor (cover identity)
Date: 10/15 10/18
Subject operates an illegal backroom poker circuit that attracts midtier executives and local power brokers. Agents cover as a wealthy gambler provided entry. On 10/16, buyins far exceeded legal thresholds, and the subject boasted of protection from “friends in city hall.”
On 10/17, an argument erupted between the subject and an enforcer over unpaid debts. Threats were made, but play resumed. On 10/18, the subject introduced an associate known as “Silver,” carrying a distinctive lighter described in other investigations. Presence of Silver indicates links to a broader network handling collections and intimidation.
Cover stable. Recommend financial surveillance on the regular players, quiet corruption probe on the purported city contacts, and a tailored raid plan that mitigates risk to civilians at the gaming site.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,12 +0,0 @@
Undercover Report Case File 72-90
Agent: Quinn (cover identity)
Date: 08/09 08/12
Subject fronts a small software firm that has attracted highrisk clients seeking datascraping and intrusion services. Agent embedded as a temporary QA tester, which offered proximity to build pipelines and the staging server. On 08/10, the subject approved a latenight push titled “crawlerplus” that contained modules for credential stuffing and proxy rotation. The code branch was kept off the main repository and shared via encrypted zip.
On 08/11, two visitors arrived with no badges and were escorted directly to the conference room. Subject requested the agent run a “sandbox smoke test” while the visitors watched a dashboard of login attempts against thirdparty targets. Conversation referenced “clean lists,” “UID harvest,” and “deliverables by Friday.”
On 08/12, the subject floated a contract expansion involving a custom build for “telecom metadata capture.” When the agent hesitated, the subject advised to “just test the pipeline; leave the contracts to me.” Security posture inside the firm is lax; logs rotate every twentyfour hours without retention.
Cover credible. Recommend rapid legal hold to preserve server images, quiet outreach to targeted platforms to harden defenses, and preparation for a coordinated search warrant before the next deliverables window.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,12 +0,0 @@
Undercover Report Case File 61-05
Agent: Carter (cover identity)
Date: 03/14 03/17
Subject operates a livestock auction and feed cooperative used as a cashhandling hub for a broader laundering scheme. Agents cover as a regional feed rep granted access to the back office and the weighticket printer. On 03/15, the subject directed staff to reprint weights for three lots and staple them over originals, inflating values by roughly thirty percent. Cash payouts were then split into smaller envelopes labeled as “hauler fees.”
On 03/16, a trailer arrived after closing with no consignor paperwork. Subject ushered the driver to the scales, then recorded a manual weight entry without zeroing the platform. Later, agent noted the same trailer departed with bales stacked higher than safety guidelines, suggesting a swap onsite.
On 03/17, the subject met with a banker in the café booth. Heard fragments: “seasonal float,” “parcel deposits,” and “dont trigger CTRs.” The banker left through the side door, avoiding cameras.
Cover remains intact. Recommend forensic review of scale logs, unannounced compliance checks tied to animalwelfare authorities as legitimate cover, and analysis of deposit structuring patterns that match the inflated weightickets.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,12 +0,0 @@
Undercover Report Case File 46-77
Agent: Ellis (cover identity)
Date: 11/08 11/11
Subject supervises a containeryard subcontractor with access to highvalue import bays. Agent entered as a craneoperator trainee assigned to night shifts. On 11/09, subject instructed the crew to reposition a fortyfooter from the manifest queue to an unnumbered slot behind the scrap pile. The container seal had a fresh dab of paint across the latch consistent with a reapplied counterfeit seal. Yard cameras were left panning, creating reliable blind spots of twenty to thirty seconds.
On 11/10, the subject met with two visitors at the fence line and exchanged a clipboard without approaching the office. Agent overheard them discuss “blank HS codes” and “holiday backlog.” The conversation implies a counterfeitelectronics shipment misdeclared to avoid inspection. Later, subject distributed burner keycards for the east pedestrian gate, instructing workers to “switch badges” if stopped.
On 11/11, a third party attempted to tip the agent to accept overtime in exchange for keeping a particular bay unsupervised. Agent declined while maintaining cover as rulebound but inexperienced. The subject appeared satisfied the lane would remain quiet.
Cover intact. Recommend customs partnership to flag the suspect container by serial, deploy a mobile Xray with minimal yard disruption, and audit badge access logs for the east gate.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,12 +0,0 @@
Undercover Report Case File 89-47
Agent: Dalton (cover identity)
Date: 10/05 10/08
Subject leads a motorcycle club that moonlights as a gunrunning and protection outfit. Agent entered as an auto detailer offering cutrate work on club vehicles. On 10/06, the subject invited the agent to a backlot barbecue where patched members traded cash for small boxes moved from saddlebag to saddlebag. A prospect muttered about “.30 cal uppers” and a “desert range test.”
On 10/07, the club sergeantatarms demanded that the agent fix a VIPs truck immediately, creating a loyalty test. While retrieving tools, the agent observed a crate under a tarp with oilpaperwrapped parts and a stencil for a defunct manufacturer. Numbers were partially filed. The subject later bragged about buyers “east of the pass.”
On 10/08, a convoy departed in staggered pairs. The subject ordered phones in airplane mode and handed out paper maps with predrawn detours. A follower crashed a bike on loose gravel; the group paused in a blind spot, repositioned a crate, and continued.
Cover remains plausible. Recommend roadside interdiction coordinated with Highway Patrol, ballistics work on recovered parts, and asset development targeting the prospect who appears chatty under alcohol.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,11 +0,0 @@
Undercover Report Case File 25-39
Agent: Doyle (cover identity)
Date: 04/03 04/06
Subject operates a small river ferry and maintenance dock that appear to facilitate contraband transfers between barges at offschedule hours. Agents cover as an outofwork boat mechanic provided routine access to bilge areas and tool lockers. On 04/04, subject instructed the crew to keep the north slip unlit after midnight and to disable the dock camera claiming a blown fuse. The camera was found unplugged rather than damaged. Two crates moved from a barge to a skiff without paperwork.
On 04/05, agent overheard the subject arguing with a radio contact over “ore purity” and “assay timing.” The phrasing suggests smuggling of highvalue metals rather than typical street narcotics. Crew members wore gloves and avoided scraping the crates corners, reinforcing the inference of dense contents. A handheld Geiger counter case was observed in the tool cage, but no reading event was witnessed.
On 04/06, the subject requested help fabricating a false enginerepair ticket to explain downtime for Barge 12. Ticket was signed under a false corporate name and backdated. Subject appeared calm after the forgery, then paid crew in cash envelopes with inconsistent denominations.
Cover intact. Recommend discreet sampling of riverbed residue near the north slip, crosschecking barge manifests for Barge 12, and coordination with environmental regulators to justify covert inspections without alerting the subject.
@@ -1,10 +0,0 @@
Undercover Report Case File 102-45
Agent: Sanders (cover identity)
Date: 08/19 08/23
Subject manages an upscale restaurant suspected of laundering illicit gambling proceeds. Agent embedded as a sommelier, granting access to private dining rooms and basement storage. On 08/20, the subject hosted three suited men; the room was sealed and guarded. While pouring, the agent heard references to “clearing accounts” and “westcoast expansion.” The subject kept a handwritten ledger with columns that do not reconcile with the pointofsale exports.
On 08/22, a delivery van bypassed normal suppliers and offloaded crates into the walkin freezer with no perishables labeling. Handlers displayed tactical posture inconsistent with food service. On 08/23, the subject warned staff about inspections and emphasized silence, revealing controlled anxiety.
Cover remains secure; agent perceived as competent and unthreatening. Recommend parallel financial audit, supplierchain verification, and a timed inspection aligned with the offcycle delivery pattern.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,12 +0,0 @@
Undercover Report Case File 81-33
Agent: Raines (cover identity)
Date: 01/18 01/21
Subject manages a boutique hotel that periodically closes floors for “private events.” Agents cover as nightshift concierge grants exposure to guest registries and keyencoder logs. On 01/19, the subject blocked out Level 5 under a shell company and delivered sealed envelopes to three suites. Noise and foot traffic suggested an invitationonly auction. Staff were instructed to use service stairs only and disable the lobby feed for two hours citing maintenance.
On 01/20, two SUVs with obscured plates arrived at the loading bay. Items resembling art crates and a locked pelican case were transferred to Suite 512. A known broker briefly appeared in the hallway, then vanished into the service lift. The minibar inventory showed no consumption despite the headcount—common during short, transactional gatherings.
On 01/21, the subject praised the agent for “discretion” and hinted at a permanent role. Later, housekeeping turned over a discarded bidder paddlestick with handwritten totals. Figures exceeded declared room revenue by a wide margin.
Cover holds. Recommend targeted warrants for the shell company, a parallel probe into the broker, and a controlled interruption on the next “private” floor closure to preserve evidence while minimizing guest disruption.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,10 +0,0 @@
Undercover Report Case File 138-24
Agent: Bennett (cover identity)
Date: 01/12 01/15
Subject manages a modest construction company that appears to use job sites for concealment and cash skimming. Agent hired as temporary labor. On 01/13, a hidden compartment beneath scaffolding yielded two heavy duffels that the subject moved to a pickup without logging materials. The foreman kept crew distant with a noisecontrol pretext.
On 01/14, the subject argued with a partner about “federal contracts” and “kickbacks,” implying bidrigging and publiccorruption exposure. On 01/15, the subject praised the agents work and hinted at “bigger projects,” a sign of attempted grooming for riskier tasks.
Cover maintained. Recommend forensic review of procurement records, site searches keyed to concealed compartments, and interviews with subcontractors likely aware of the skimming practice.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,10 +0,0 @@
Undercover Report Case File 110-19
Agent: Mitchell (cover identity)
Date: 09/07 09/10
Subject runs a repair garage that doubles as a weapons workshop. Agent embedded as a parttime apprentice. On 09/08, the subject modified rifles at a back bench outfitted with jigs, thread cutters, and solvent traps. Multiple unserialized receivers were visible. The subject bragged about clients who “pay for silence.”
On 09/09, a phone call arranged delivery of “three crates” to a desert rendezvous, specifying urgency and “militarygrade parts.” On 09/10, the subject tested the agent with a drop of a box labeled scrap to an abandoned rail yard. The weight and rattle suggested mixed metal components. Delivery completed under surveillance without opening to preserve cover.
Cover intact though scrutiny is increasing. Recommend immediate tracing of the railyard contact, controlled intercept of the desert transfer, and NIBIN checks on recovered parts.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,11 +0,0 @@
Undercover Report Case File 07-14
Agent: Mason (cover identity)
Date: 06/12 06/15
Subject continues to operate a cashheavy nightclub believed to be central to midlevel narcotics distribution in the northwest sector. Cover identity as parttime bartender remains effective and provides nightly access to storeroom traffic and the restricted back hallway behind the stage. On 06/13, the subject supervised delivery of three sealed cartons labeled as beverage syrups. The weight and the handlers posture suggested dense contents inconsistent with that label. Subject instructed all staff to avoid contact and to log the cartons as vendor overstock without serials.
On 06/14, the subject held a closeddoor conversation with a courier known locally as “Rico.” Portions overheard included references to a “Miami truck” and “eastcoast push.” Tone implied urgency and reliance on corrupt support, described as “our friend at permits.” Identity of the alleged official remains unknown. Later that evening, a floor manager tested staff loyalty by asking casual questions about police patrol patterns near 12th Street.
On 06/15, subject gave the agent an envelope for a routine offsite drop. Envelope was briefly photographed under pretext of retrieving car keys; contents included a short ledger with coded initials, tally marks, and four phone numbers written in alternating ink colors. Envelope was delivered intact to avoid suspicion. Subjects postdrop demeanor appeared relaxed, suggesting the transaction cleared immediate obligations.
Cover remains intact, with agent perceived as reliable but peripheral. Recommend discreet financial analysis of the ledger numbers, targeted canvass for the unknown permits contact, and coordination with portofentry teams to watch for southtonorth freight aligning with subjects timetable.
@@ -1,12 +0,0 @@
Undercover Report Case File 92-14
Agent: Langston (cover identity)
Date: 05/10 05/14
Subject continues to expand operations through a network of import/export fronts in the industrial docklands. Cover identity as an independent freight consultant has provided access to routing meetings and latenight inspections. On 05/11, the subject oversaw containers labeled “agricultural machinery” with weights above manifest and brandnew locks. Handlers were armed and avoided eye contact. No safe chance to open containers without exposure.
On 05/12, the subject introduced an associate, “Gallo,” who quizzed the agent on Gulfport customs routines and inspection frequencies. Questions implied an imminent push of highvalue contraband through maritime lanes with compromised timing windows. The crew canceled two runs after learning of a regional seizure, and the subject complained about “partners overseas losing patience.”
On 05/14, a briefcase exchange at a dockside café lasted under two minutes; the subject accepted the case and passed it to Gallo without inspection. Both appeared tense but determined.
Cover remains stable yet fragile. Recommend expanded surveillance at the targeted warehouses, customs liaisons to flag the specific manifest anomalies noted, and a rapid extraction plan if the crew pivots to violent vetting.
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
@@ -1,20 +0,0 @@
095_07-14.txt 07-14 Mason
007_19-52.txt 19-52 Harper
083_25-39.txt 25-39 Doyle
019_31-11.txt 31-11 Vega
075_46-77.txt 46-77 Ellis
031_53-28.txt 53-28 Navarro
072_61-05.txt 61-05 Carter
064_72-90.txt 72-90 Quinn
091_81-33.txt 81-33 Raines
079_89-47.txt 89-47 Dalton
098_92-14.txt 92-14 Langston
012_94-31.txt 94-31 Donovan
016_98-07.txt 98-07 Fletcher
087_102-45.txt 102-45 Sanders
094_110-19.txt 110-19 Mitchell
063_117-56.txt 117-56 OConnor
017_123-88.txt 123-88 Holloway
011_130-62.txt 130-62 Lennox
093_138-24.txt 138-24 Bennett
001_145-93.txt 145-93 Carver
+3
View File
@@ -0,0 +1,3 @@
body {
background-color: skyblue;
}
-66
View File
@@ -1,66 +0,0 @@
// Brand palette derived from public/images/logo.png (teal key/globe on dark navy)
$brand-teal: #2f8fae;
$brand-teal-dark: #1c5a70;
$brand-teal-light:#6fb8d1;
$brand-navy: #0d1f26;
$brand-navy-soft: #16323f;
$brand-bg: #f4f8f9;
// Bootstrap variable overrides (must come before the bootstrap import)
$primary: $brand-teal;
$secondary: $brand-navy-soft;
$dark: $brand-navy;
$body-bg: $brand-bg;
$body-color: $brand-navy-soft;
$link-color: $brand-teal;
$link-hover-color: $brand-teal-dark;
$border-radius: .5rem;
@import "bootstrap/scss/bootstrap";
@import "bootstrap-icons/font/bootstrap-icons.css";
:root {
--brand-teal: #{$brand-teal};
--brand-teal-dark: #{$brand-teal-dark};
--brand-navy: #{$brand-navy};
--brand-navy-soft: #{$brand-navy-soft};
--brand-bg: #{$brand-bg};
}
body {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.site-main {
flex: 1 0 auto;
}
.site-header {
background: linear-gradient(90deg, $brand-navy, $brand-navy-soft);
}
.site-header .navbar-brand {
display: flex;
align-items: center;
gap: .5rem;
font-weight: 700;
letter-spacing: .02em;
}
.site-header .navbar-brand img {
height: 36px;
width: auto;
}
.site-footer {
flex-shrink: 0;
background: $brand-navy;
color: rgba(255, 255, 255, .65);
}
.auth-card {
max-width: 440px;
margin: 3rem auto;
}
+11 -136
View File
@@ -1,142 +1,17 @@
/* Styles specific to Game1 */
/* page-level indicator to confirm CSS is loaded */
/* Custom scrollbar for WebKit browsers */
html::-webkit-scrollbar,
body::-webkit-scrollbar {
width: 1px;
body.game1-page {
/* subtle background tint so you can visually confirm on /game */
background-color: #f9fbff;
}
html::-webkit-scrollbar-track,
body::-webkit-scrollbar-track {
background: #000;
}
html::-webkit-scrollbar-thumb,
body::-webkit-scrollbar-thumb {
background: #F00;
}
/* Standard properties for Firefox */
body {
background-color: #000;
min-height: 100vh;
font-family: monospace;
margin: 0;
scrollbar-width: thin;
scrollbar-color: #F00 #000;
}
div#game-timer {
position: fixed;
top: 0;
left: 0;
width: 100%;
padding: 20px;
background-color: #000;
color: #F00;
font-size: 28px;
z-index: 100;
}
div#message-container {
padding: 20px;
padding-top: 80px; /* Space for fixed timer */
display: flex;
flex-direction: column;
justify-content: flex-end;
min-height: calc(100vh - 100px); /* Fill most of the viewport initially */
box-sizing: border-box;
font-size: 14px;
}
div.message {
color: #C0C0C0;
white-space: pre-wrap;
line-height: 1.35;
margin-bottom: 2px;
}
div.message-virus {
color: #F00;
font-weight: bold;
}
div.message-mainframe {
color: #0F0;
}
div.message-hint {
color: #FF0;
font-weight: bold;
}
div#lock-banner {
position: fixed;
top: 68px;
left: 0;
width: 100%;
padding: 12px 20px;
background-color: #200;
border-top: 1px solid #F00;
border-bottom: 1px solid #F00;
color: #F00;
font-size: 18px;
font-weight: bold;
letter-spacing: 1px;
z-index: 99;
display: flex;
justify-content: space-between;
align-items: center;
animation: lock-banner-pulse 1s ease-in-out infinite;
}
@keyframes lock-banner-pulse {
0%, 100% {
background-color: #200;
}
50% {
background-color: #400;
}
}
body.locked div#message-container {
padding-top: 130px;
}
div#input {
padding: 20px;
}
input#input-message {
width: 100%;
padding: 6px 10px;
background: #111;
border: 1px solid #A00000;
color: #C0C0C0;
font-size: 14px;
box-sizing: border-box;
font-family: monospace;
}
.flash-red::after {
content: '';
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100px;
pointer-events: none;
background: linear-gradient(to top, rgba(255, 0, 0, 0.8), transparent);
animation: flash-red-anim 0.1s linear forwards;
z-index: 9999;
}
@keyframes flash-red-anim {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
/* example component style */
.game1-banner {
padding: 1rem 1.25rem;
border: 1px solid #cfe2ff;
background: #e9f2ff;
color: #0b5ed7;
border-radius: 8px;
margin: 1rem 0;
}
+7
View File
@@ -0,0 +1,7 @@
services:
###> symfony/mercure-bundle ###
mercure:
ports:
- "80"
###< symfony/mercure-bundle ###
+31
View File
@@ -0,0 +1,31 @@
services:
###> symfony/mercure-bundle ###
mercure:
image: dunglas/mercure
restart: unless-stopped
environment:
# Uncomment the following line to disable HTTPS,
#SERVER_NAME: ':80'
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
# Set the URL of your Symfony project (without trailing slash!) as value of the cors_origins directive
MERCURE_EXTRA_DIRECTIVES: |
cors_origins http://127.0.0.1:8000
# Comment the following line to disable the development mode
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
healthcheck:
test: ["CMD", "curl", "-f", "https://localhost/healthz"]
timeout: 5s
retries: 5
start_period: 60s
volumes:
- mercure_data:/data
- mercure_config:/config
###< symfony/mercure-bundle ###
volumes:
###> symfony/mercure-bundle ###
mercure_data:
mercure_config:
###< symfony/mercure-bundle ###
+49 -51
View File
@@ -7,50 +7,48 @@
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/dbal": "^3.10.5",
"doctrine/doctrine-bundle": "^2.18.3",
"doctrine/doctrine-migrations-bundle": "^3.7",
"doctrine/orm": "^3.6.7",
"karser/karser-recaptcha3-bundle": "^0.3.0",
"phpdocumentor/reflection-docblock": "^5.6.7",
"phpstan/phpdoc-parser": "^2.3.2",
"symfony/asset": "7.4.*",
"symfony/asset-mapper": "7.4.*",
"symfony/console": "7.4.*",
"symfony/doctrine-messenger": "7.4.*",
"symfony/dotenv": "7.4.*",
"symfony/expression-language": "7.4.*",
"symfony/flex": "^2.11",
"symfony/form": "7.4.*",
"symfony/framework-bundle": "7.4.*",
"symfony/http-client": "7.4.*",
"symfony/intl": "7.4.*",
"symfony/mailer": "7.4.*",
"symfony/mailgun-mailer": "7.4.*",
"symfony/mercure-bundle": "^0.3.9",
"symfony/mime": "7.4.*",
"symfony/monolog-bundle": "^3.11.2",
"symfony/notifier": "7.4.*",
"symfony/process": "7.4.*",
"symfony/property-access": "7.4.*",
"symfony/property-info": "7.4.*",
"symfony/rate-limiter": "7.4.*",
"symfony/runtime": "7.4.*",
"symfony/security-bundle": "7.4.*",
"symfony/serializer": "7.4.*",
"symfony/stimulus-bundle": "^2.36",
"symfony/string": "7.4.*",
"symfony/translation": "7.4.*",
"symfony/twig-bundle": "7.4.*",
"symfony/ux-turbo": "^2.36",
"symfony/validator": "7.4.*",
"symfony/web-link": "7.4.*",
"symfony/webpack-encore-bundle": "^2.4.1",
"symfony/yaml": "7.4.*",
"symfonycasts/reset-password-bundle": "^1.25",
"doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.16",
"doctrine/doctrine-migrations-bundle": "^3.4",
"doctrine/orm": "^3.5",
"phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.3",
"symfony/asset": "7.3.*",
"symfony/asset-mapper": "7.3.*",
"symfony/console": "7.3.*",
"symfony/doctrine-messenger": "7.3.*",
"symfony/dotenv": "7.3.*",
"symfony/expression-language": "7.3.*",
"symfony/flex": "^2",
"symfony/form": "7.3.*",
"symfony/framework-bundle": "7.3.*",
"symfony/http-client": "7.3.*",
"symfony/intl": "7.3.*",
"symfony/mailer": "7.3.*",
"symfony/mercure-bundle": "^0.3",
"symfony/mime": "7.3.*",
"symfony/monolog-bundle": "^3.0",
"symfony/notifier": "7.3.*",
"symfony/process": "7.3.*",
"symfony/property-access": "7.3.*",
"symfony/property-info": "7.3.*",
"symfony/runtime": "7.3.*",
"symfony/security-bundle": "7.3.*",
"symfony/sendgrid-mailer": "7.3.*",
"symfony/serializer": "7.3.*",
"symfony/stimulus-bundle": "^2.30",
"symfony/string": "7.3.*",
"symfony/translation": "7.3.*",
"symfony/twig-bundle": "7.3.*",
"symfony/ux-turbo": "^2.30",
"symfony/validator": "7.3.*",
"symfony/web-link": "7.3.*",
"symfony/webpack-encore-bundle": "^2.1",
"symfony/yaml": "7.3.*",
"symfonycasts/reset-password-bundle": "^1.24",
"symfonycasts/verify-email-bundle": "^1.18",
"twig/extra-bundle": "^2.12|^3.24",
"twig/twig": "^2.12|^3.28.0"
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
},
"config": {
"allow-plugins": {
@@ -100,16 +98,16 @@
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.4.*"
"require": "7.3.*"
}
},
"require-dev": {
"phpunit/phpunit": "^11.5.55",
"symfony/browser-kit": "7.4.*",
"symfony/css-selector": "7.4.*",
"symfony/debug-bundle": "7.4.*",
"symfony/maker-bundle": "^1.67",
"symfony/stopwatch": "7.4.*",
"symfony/web-profiler-bundle": "7.4.*"
"phpunit/phpunit": "^11.5",
"symfony/browser-kit": "7.3.*",
"symfony/css-selector": "7.3.*",
"symfony/debug-bundle": "7.3.*",
"symfony/maker-bundle": "^1.0",
"symfony/stopwatch": "7.3.*",
"symfony/web-profiler-bundle": "7.3.*"
}
}
Generated
+1143 -1443
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -17,5 +17,4 @@ return [
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true],
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
Karser\Recaptcha3Bundle\KarserRecaptcha3Bundle::class => ['all' => true],
];
+9 -8
View File
@@ -1,10 +1,11 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
# form:
# csrf_protection:
# token_id: submit
# csrf_protection:
# stateless_token_ids:
# - submit
# - authenticate
# - logout
form:
csrf_protection:
token_id: submit
csrf_protection:
stateless_token_ids:
- submit
- authenticate
- logout
+1 -9
View File
@@ -1,14 +1,6 @@
doctrine:
dbal:
# url: '%env(resolve:DATABASE_URL)%'
driver: '%env(DB_DRIVER)%'
server_version: '%env(DB_SERVER_VERSION)%'
host: '%env(DB_HOST)%'
port: '%env(DB_PORT)%'
user: '%env(DB_USER)%'
password: '%env(DB_PASSWORD)%'
dbname: '%env(DB_NAME)%'
charset: '%env(DB_CHARSET)%'
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
+1 -21
View File
@@ -8,27 +8,7 @@ framework:
fallbacks: ['en', 'nl']
# Note that the session will be started ONLY if you read or write from it.
session:
handler_id: null
cookie_secure: auto
cookie_samesite: lax
storage_factory_id: session.storage.factory.native
save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
rate_limiter:
invite_code_join:
policy: 'sliding_window'
limit: 10
interval: '1 minute'
when@prod:
framework:
session:
handler_id: null
cookie_secure: true
cookie_samesite: lax
storage_factory_id: session.storage.factory.native
save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
session: true
#esi: true
#fragments: true
-5
View File
@@ -1,5 +0,0 @@
karser_recaptcha3:
site_key: '%env(RECAPTCHA3_KEY)%'
secret_key: '%env(RECAPTCHA3_SECRET)%'
score_threshold: 0.5
enabled: true
+1 -3
View File
@@ -3,6 +3,4 @@ mercure:
default:
url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%'
jwt:
secret: '%env(MERCURE_JWT_SECRET)%'
publish: ['*']
jwt: '%env(MERCURE_JWT_SECRET)%'
+1 -1
View File
@@ -26,4 +26,4 @@ framework:
Symfony\Component\Notifier\Message\SmsMessage: async
# Route your messages to the transports
'App\Tech\Message\ProcessTaskMessage': async
# 'App\Message\YourMessage': async
+1 -16
View File
@@ -47,14 +47,6 @@ when@prod:
excluded_http_codes: [404, 405]
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
nested:
type: group
members: [nested_file, nested_stderr]
nested_file:
type: stream
path: "%kernel.logs_dir%/php/prod.log"
level: debug
formatter: monolog.formatter.json
nested_stderr:
type: stream
path: php://stderr
level: debug
@@ -64,14 +56,7 @@ when@prod:
process_psr_3_messages: false
channels: ["!event", "!doctrine"]
deprecation:
type: group
type: stream
channels: [deprecation]
members: [deprecation_file, deprecation_stderr]
deprecation_file:
type: stream
path: "%kernel.logs_dir%/php/deprecation.log"
formatter: monolog.formatter.json
deprecation_stderr:
type: stream
path: php://stderr
formatter: monolog.formatter.json
+1
View File
@@ -2,6 +2,7 @@ framework:
notifier:
chatter_transports:
texter_transports:
sendgrid: '%env(MAILER_DSN)%'
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
+1 -1
View File
@@ -2,7 +2,7 @@ framework:
router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
default_uri: '%env(SITE_BASE_URL)%'
#default_uri: http://localhost
when@prod:
framework:
-4
View File
@@ -22,9 +22,6 @@ security:
enable_csrf: true
username_parameter: username
password_parameter: password
login_throttling:
max_attempts: 5
interval: '15 minutes'
logout:
path: app_logout
# where to redirect after logout
@@ -33,7 +30,6 @@ security:
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/, roles: PUBLIC_ACCESS, requires_channel: https }
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
@@ -1,2 +0,0 @@
karser_recaptcha3:
enabled: false
-1
View File
@@ -1,5 +1,4 @@
twig:
form_themes: ['bootstrap_5_layout.html.twig']
globals:
mercure_public_url: '%env(MERCURE_PUBLIC_URL)%'
mercure_topic_base: '%env(MERCURE_TOPIC_BASE)%'
-5
View File
@@ -4,7 +4,6 @@
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
mailer_from: '%env(MAILER_FROM)%'
services:
# default configuration for services in *this* file
@@ -17,9 +16,5 @@ services:
App\:
resource: '../src/'
App\Game\Service\GameResponseService:
arguments:
$projectDir: '%kernel.project_dir%'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
+1 -1
View File
@@ -4,7 +4,7 @@ Use this index to quickly locate files and directories during development and in
## Top-Level
- docker/compose.yaml / docker/compose.override.yaml — Docker services.
- docker/ — Docker build contexts and configs (php Dockerfile, nginx vhost).
- docker/ — Docker build contexts and configs (php Dockerfile, nginx vhost, compose files).
- composer.json / composer.lock — Dependencies and scripts.
- importmap.php — Importmap configuration for JS dependencies.
- phpunit.dist.xml — PHPUnit configuration.
+9 -15
View File
@@ -9,7 +9,7 @@ This app can run fully in Docker using docker compose with PHP-FPM, Nginx and My
- mailer (dev only via compose.override.yaml): Mailpit (SMTP/UI)
## Prerequisites
- Docker and Docker Compose (docker compose)
- Docker and Docker Compose (v2)
## Usage
@@ -21,42 +21,36 @@ App will be served at http://localhost:8080
Alternatively (manual):
```
cd docker
docker compose up -d --build
docker compose -f docker/compose.yaml -f docker/compose.override.yaml up -d --build
```
### 2) Install dependencies
The setup script already runs composer install. To run manually:
```
cd docker
docker compose exec php composer install
docker compose -f docker/compose.yaml -f docker/compose.override.yaml exec php composer install
```
### 3) Prepare DB
The setup script already prepares the DB. To run manually:
```
cd docker
docker compose exec php php bin/console doctrine:database:create --if-not-exists
docker compose exec php php bin/console doctrine:migrations:migrate -n
docker compose -f docker/compose.yaml -f docker/compose.override.yaml exec php php bin/console doctrine:database:create --if-not-exists
docker compose -f docker/compose.yaml -f docker/compose.override.yaml exec php php bin/console doctrine:migrations:migrate -n
```
### 4) Run tests
```
cd docker
docker compose exec php vendor/bin/phpunit
docker compose -f docker/compose.yaml -f docker/compose.override.yaml exec php vendor/bin/phpunit
```
### 5) Logs
```
cd docker
docker compose logs -f nginx
docker compose logs -f php
docker compose -f docker/compose.yaml -f docker/compose.override.yaml logs -f nginx
docker compose -f docker/compose.yaml -f docker/compose.override.yaml logs -f php
```
### 6) Stop
```
cd docker
docker compose down
docker compose -f docker/compose.yaml -f docker/compose.override.yaml down
```
## Notes
+11 -14
View File
@@ -1,9 +1,9 @@
# Email Delivery: Dev Mailcatcher & Production Mailgun
# Email Delivery: Dev Mailcatcher & Production SendGrid
This application uses Symfony Mailer. We separate development and production delivery:
- Development: Mailpit (mailcatcher) via SMTP in Docker.
- Production: Mailgun via API transport.
- Production: SendGrid via API transport.
## Development (Mailpit)
@@ -18,31 +18,28 @@ MAILER_DSN=smtp://mailer:1025
```
- Usage:
1. Start stack: `docker-compose up -d`
1. Start stack: `docker compose up -d`
2. Send an email from the app.
3. Open http://localhost:8025 to view captured emails.
## Production (Mailgun)
## Production (SendGrid)
Use the Mailgun API transport (`symfony/mailgun-mailer` bridge). Do not commit secrets.
Use the SendGrid API transport. Do not commit secrets.
- Example configuration is in `.env.prod`:
```
MAILER_DSN=mailgun+api://${MAILGUN_API_KEY}:${MAILGUN_DOMAIN}@default?region=eu
MAILER_DSN=sendgrid+api://%env(resolve:SENDGRID_API_KEY)%@default
```
- `region=eu` is only needed if the Mailgun account/domain was created in Mailgun's EU region (common for `.nl`/EU-based senders). Drop it (or use `region=us`) if the domain lives in the US region.
- Provide `MAILGUN_API_KEY` and `MAILGUN_DOMAIN` via:
- Real environment variables on the server/container, or
- Symfony secrets: `php bin/console secrets:set MAILGUN_API_KEY` (and dump for prod), or
- Provide `SENDGRID_API_KEY` via:
- Real environment variable on the server/container, or
- Symfony secrets: `php bin/console secrets:set SENDGRID_API_KEY` (and dump for prod), or
- Orchestration secret stores (e.g., Docker/K8s).
- The sending domain must be added and DNS-verified (SPF/DKIM/tracking CNAME) in the Mailgun dashboard before production sending will work reliably; unverified domains are rate-limited/sandboxed.
### Notes
- No Mailpit container is defined in the base `compose.yaml`, only in `compose.override.yaml`. This ensures it is used in development only.
- To test email locally without Docker, you can:
- Run Mailpit on your host (ports 1025/8025) and set `MAILER_DSN=smtp://127.0.0.1:1025` in `.env.local`.
- If you need to use Mailgun SMTP instead of API, a DSN example:
`mailgun+smtp://USERNAME:PASSWORD@default?region=eu` (username/password come from the Mailgun domain's SMTP credentials).
- Use `php bin/console app:mail:test you@example.com` to send a quick test email against whatever `MAILER_DSN` is currently configured.
- If you need to use SendGrid SMTP instead of API, a DSN example:
`smtp://apikey:YOUR_SENDGRID_API_KEY@smtp.sendgrid.net:587`.
-31
View File
@@ -1,31 +0,0 @@
# User and Group IDs
USER_ID=1000
GROUP_ID=1000
# Application
APP_ENV=prod
SITE_BASE_URL=https://escapepage.com
# Mailer
MAILGUN_API_KEY=CHANGEME_MAILGUN_API_KEY
MAILGUN_DOMAIN=CHANGEME_MAILGUN_DOMAIN
MAILER_DSN=mailgun+api://CHANGEME_MAILGUN_API_KEY:CHANGEME_MAILGUN_DOMAIN@default?region=eu
MAILER_FROM=mailer@escapepage.nl
# Database
DATABASE_URL=mysql://escapepage:CHANGEME_DB_PASSWORD@database:3306/escapepage?serverVersion=8.0.32&charset=utf8mb4
DB_NAME=escapepage
DB_USER=escapepage
DB_PASSWORD=CHANGEME_DB_PASSWORD
MYSQL_ROOT_PASSWORD=CHANGEME_MYSQL_ROOT_PASSWORD
# Mercure
MERCURE_URL=http://mercure/.well-known/mercure
MERCURE_PUBLIC_URL=https://mercure.escapepage.com/.well-known/mercure
MERCURE_JWT_SECRET=CHANGEME_MERCURE_JWT_SECRET
MERCURE_CORS_ALLOWED_ORIGINS=https://www.escapepage.com https://escapepage.com
MERCURE_TOPIC_BASE=https://escapepage.com
# Recaptcha
RECAPTCHA3_KEY=CHANGEME_RECAPTCHA3_KEY
RECAPTCHA3_SECRET=CHANGEME_RECAPTCHA3_SECRET
+8 -20
View File
@@ -1,19 +1,20 @@
services:
php:
environment:
XDEBUG_MODE: "off"
XDEBUG_MODE: off
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- mailer
# networks:
# backend:
# ipv4_address: 172.23.0.10
###> doctrine/doctrine-bundle ###
###< doctrine/doctrine-bundle ###
###> doctrine/doctrine-bundle ###
database:
ports:
- "3306"
###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
###> symfony/mailer ###
mailer:
image: axllent/mailpit
ports:
@@ -22,17 +23,4 @@ services:
environment:
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
# networks:
# backend:
# ipv4_address: 172.23.0.13
# networks:
# backend:
# name: escapepage_network
# driver: bridge
# ipam:
# config:
# - subnet: 172.23.0.0/16
# gateway: 172.23.0.1
# attachable: true
###< symfony/mailer ###
+32 -125
View File
@@ -1,102 +1,21 @@
version: '3.7'
services:
php:
build:
context: ..
dockerfile: docker/php/Dockerfile
args:
USER_ID: ${USER_ID}
GROUP_ID: ${GROUP_ID}
dockerfile: php/Dockerfile
container_name: escapepage-php
volumes:
- ../:/var/www/html:delegated
- /etc/hosts:/etc/hosts:ro
environment:
APP_ENV: ${APP_ENV}
SITE_BASE_URL: ${SITE_BASE_URL}
MAILER_DSN: ${MAILER_DSN}
MAILER_FROM: ${MAILER_FROM}
DATABASE_URL: ${DATABASE_URL}
MERCURE_URL: ${MERCURE_URL}
MERCURE_PUBLIC_URL: ${MERCURE_PUBLIC_URL}
MERCURE_JWT_SECRET: ${MERCURE_JWT_SECRET}
MERCURE_CORS_ALLOWED_ORIGINS: ${MERCURE_CORS_ALLOWED_ORIGINS}
MERCURE_TOPIC_BASE: ${MERCURE_TOPIC_BASE}
RECAPTCHA3_KEY: ${RECAPTCHA3_KEY}
RECAPTCHA3_SECRET: ${RECAPTCHA3_SECRET}
APP_ENV: dev
depends_on:
- database
- mercure
# networks:
# backend:
# ipv4_address: 172.23.0.10
restart: unless-stopped
php-worker:
build:
context: ..
dockerfile: docker/php/Dockerfile
args:
USER_ID: ${USER_ID}
GROUP_ID: ${GROUP_ID}
container_name: escapepage-php-worker
volumes:
- ../:/var/www/html:delegated
- /etc/hosts:/etc/hosts:ro
environment:
APP_ENV: ${APP_ENV}
SITE_BASE_URL: ${SITE_BASE_URL}
MAILER_DSN: ${MAILER_DSN}
MAILER_FROM: ${MAILER_FROM}
DATABASE_URL: ${DATABASE_URL}
MERCURE_URL: ${MERCURE_URL}
MERCURE_PUBLIC_URL: ${MERCURE_PUBLIC_URL}
MERCURE_JWT_SECRET: ${MERCURE_JWT_SECRET}
MERCURE_CORS_ALLOWED_ORIGINS: ${MERCURE_CORS_ALLOWED_ORIGINS}
MERCURE_TOPIC_BASE: ${MERCURE_TOPIC_BASE}
RECAPTCHA3_KEY: ${RECAPTCHA3_KEY}
RECAPTCHA3_SECRET: ${RECAPTCHA3_SECRET}
depends_on:
- database
- mercure
command: ["php", "bin/console", "messenger:consume", "async", "-vv"]
# networks:
# backend:
# ipv4_address: 172.23.0.11
restart: unless-stopped
php-cron:
build:
context: ..
dockerfile: docker/php/Dockerfile
args:
USER_ID: ${USER_ID}
GROUP_ID: ${GROUP_ID}
container_name: escapepage-php-cron
volumes:
- ../:/var/www/html:delegated
- /etc/hosts:/etc/hosts:ro
environment:
APP_ENV: ${APP_ENV}
SITE_BASE_URL: ${SITE_BASE_URL}
MAILER_DSN: ${MAILER_DSN}
MAILER_FROM: ${MAILER_FROM}
DATABASE_URL: ${DATABASE_URL}
MERCURE_URL: ${MERCURE_URL}
MERCURE_PUBLIC_URL: ${MERCURE_PUBLIC_URL}
MERCURE_JWT_SECRET: ${MERCURE_JWT_SECRET}
MERCURE_CORS_ALLOWED_ORIGINS: ${MERCURE_CORS_ALLOWED_ORIGINS}
MERCURE_TOPIC_BASE: ${MERCURE_TOPIC_BASE}
RECAPTCHA3_KEY: ${RECAPTCHA3_KEY}
RECAPTCHA3_SECRET: ${RECAPTCHA3_SECRET}
depends_on:
- database
- mercure
command: ["crond", "-f", "-l", "2"]
# networks:
# backend:
# ipv4_address: 172.23.0.16
networks:
- backend
restart: unless-stopped
nginx:
@@ -104,17 +23,13 @@ services:
container_name: escapepage-nginx
ports:
- "8080:80"
- "8443:443"
volumes:
- ../:/var/www/html:ro
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- /etc/hosts:/etc/hosts:ro
depends_on:
- php
# networks:
# backend:
# ipv4_address: 172.23.0.12
networks:
- backend
restart: unless-stopped
mailer:
@@ -122,68 +37,60 @@ services:
container_name: escapepage-mailer
ports:
- "8025:8025"
volumes:
- /etc/hosts:/etc/hosts:ro
networks:
- default
- nginx_proxy
- backend
restart: unless-stopped
mercure:
image: dunglas/mercure:v0.21
container_name: escapepage-mercure
environment:
SERVER_NAME: "http://:80"
MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_SECRET}
MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_JWT_SECRET}
MERCURE_CORS_ALLOWED_ORIGINS: ${MERCURE_CORS_ALLOWED_ORIGINS}
MERCURE_PUBLISH_ALLOWED_ORIGINS: ${MERCURE_CORS_ALLOWED_ORIGINS}
SERVER_NAME: ":80"
MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_SECRET:-!ChangeThisMercureJWT!}
MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_JWT_SECRET:-!ChangeThisMercureJWT!}
MERCURE_CORS_ALLOWED_ORIGINS: http://localhost:8080
MERCURE_PUBLISH_ALLOWED_ORIGINS: http://localhost:8080
MERCURE_EXTRA_DIRECTIVES: |
cors_origins ${MERCURE_CORS_ALLOWED_ORIGINS}
publish_origins ${MERCURE_CORS_ALLOWED_ORIGINS}
cors_origins http://localhost:8080
# Allow anonymous subscribers in dev only
anonymous
ports:
- "8090:80"
volumes:
- /etc/hosts:/etc/hosts:ro
networks:
- default
- nginx_proxy
- backend
restart: unless-stopped
###> doctrine/doctrine-bundle ###
###> doctrine/doctrine-bundle ###
database:
image: mysql:8.0
container_name: escapepage-db
environment:
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE:-app}
MYSQL_USER: ${MYSQL_USER:-app}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-!ChangeMe!}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
command: ["--default-authentication-plugin=mysql_native_password", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci", "--lower-case-table-names=1", "--innodb-use-native-aio=0"]
command: ["--default-authentication-plugin=mysql_native_password", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"]
volumes:
- ../var/volumes/db:/var/lib/mysql:rw
- ./mysql/init:/docker-entrypoint-initdb.d:ro
- /etc/hosts:/etc/hosts:ro
- database_data:/var/lib/mysql:rw
# Uncomment the two lines below if you need to access MySQL from your host (workbench, etc.)
ports:
- "3306:3306"
# networks:
# backend:
# ipv4_address: 172.23.0.15
# ports:
# - "3306:3306"
networks:
- backend
restart: unless-stopped
###< doctrine/doctrine-bundle ###
volumes:
###> doctrine/doctrine-bundle ###
database_data:
###< doctrine/doctrine-bundle ###
networks:
nginx_proxy:
external: true
name: nginx_default
backend:
driver: bridge
-5
View File
@@ -1,5 +0,0 @@
-- This script ensures the user has correct privileges.
-- The user is actually created by the official MySQL image using environment variables.
GRANT ALL PRIVILEGES ON *.* TO 'escapepage'@'%';
FLUSH PRIVILEGES;
-20
View File
@@ -1,18 +1,6 @@
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name _;
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
root /var/www/html/public;
index index.php index.html;
@@ -30,14 +18,6 @@ server {
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_pass php:9000;
fastcgi_read_timeout 120;
# Ensure HTTPS is correctly detected by Symfony if Nginx is behind a TLS termination proxy
fastcgi_param HTTPS $https if_not_empty;
# Standard forwarded headers
fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;
fastcgi_param HTTP_X_FORWARDED_PROTO $scheme;
fastcgi_param HTTP_X_FORWARDED_HOST $host;
fastcgi_param HTTP_X_FORWARDED_PORT $server_port;
}
location ~ /\.ht {
-22
View File
@@ -1,22 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDrTCCApWgAwIBAgIURXHwywjcTFR43Q8+qtMAMuhHmW0wDQYJKoZIhvcNAQEL
BQAwZjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
MRUwEwYDVQQKDAxPcmdhbml6YXRpb24xDTALBgNVBAsMBFVuaXQxEjAQBgNVBAMM
CWxvY2FsaG9zdDAeFw0yNjAxMTAxMjMzNTNaFw0yNzAxMTAxMjMzNTNaMGYxCzAJ
BgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
CgwMT3JnYW5pemF0aW9uMQ0wCwYDVQQLDARVbml0MRIwEAYDVQQDDAlsb2NhbGhv
c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0dQIpm6SeY/Qt1zTr
GDfQuRAqowde6vzlNDwwC5hNQUaA4MCsDcmqmxj/YPUA8qG4MWQzYsj3HEn8l863
a7BELIYy2kvHTO7mgZMsBiH6HzHilIOsZkMJEV3QLlFn7VRb7i6WSw48pbRJk77l
sOX/e3vzE2pemnx4ggSORzNorrQ7UwyBpK374yisKSFzs6KKPnkVDbfBNX2k+fUT
8Ncjq5WkllA93ztPzh1iHNcFThx+MiH5fcs9obdMbfNkcQy22J9Nbi0OT9Tf8R7k
OaBEVPxFkT+moj6bCwetLkdQDGaoGA6AXTR1lrN812eU1TJ6KA4TAOj4ZAuygWa0
kqi3AgMBAAGjUzBRMB0GA1UdDgQWBBSayyPInKCPbaliYycRx9GEK2tTFjAfBgNV
HSMEGDAWgBSayyPInKCPbaliYycRx9GEK2tTFjAPBgNVHRMBAf8EBTADAQH/MA0G
CSqGSIb3DQEBCwUAA4IBAQCT3r5wZd8fN/ognHFopJRKxjw3ZBBYl54ELb32OSVS
NcKR63/2kZc7KQY5LjPbBMpDutLUPsVtJ97OSYY/JQDm/VVkJy0jIUtPD/bLnjEI
bhMoIGKwUDtnSaYF3oXhwMX3XchDCLmpsk+E17LTTq+tHUzkhXZu+sHoHrE70Wls
XfziM0O/zpApJQSeCLi8UDGffLVChFQd4uU//YW+4OMyk/mbu7dV4ckJXQVIvqTr
7UuC7SgRChcYkaQpkDUnaoX+miKbr9SHUmBSbCsXDyPDth5TOUSZWbP6ewDKVWW7
37OURA5UqT2RvnX75+FdLnBtqJrt/3X8wafOOLXILwmA
-----END CERTIFICATE-----
-28
View File
@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0dQIpm6SeY/Qt
1zTrGDfQuRAqowde6vzlNDwwC5hNQUaA4MCsDcmqmxj/YPUA8qG4MWQzYsj3HEn8
l863a7BELIYy2kvHTO7mgZMsBiH6HzHilIOsZkMJEV3QLlFn7VRb7i6WSw48pbRJ
k77lsOX/e3vzE2pemnx4ggSORzNorrQ7UwyBpK374yisKSFzs6KKPnkVDbfBNX2k
+fUT8Ncjq5WkllA93ztPzh1iHNcFThx+MiH5fcs9obdMbfNkcQy22J9Nbi0OT9Tf
8R7kOaBEVPxFkT+moj6bCwetLkdQDGaoGA6AXTR1lrN812eU1TJ6KA4TAOj4ZAuy
gWa0kqi3AgMBAAECggEAfwOccgzK4XEY/OrspEx3fMHFTz1Qgs6DEhCiDG8c08OO
DEglVPSfbSWdgqKL0A73JN4e2Mw/By8yJEf1h8SUXGe6TTC5BZ5wyG2LWQE4CQTL
598AjuerZ0aB8XWodq3lIo+S2tYZPzainucPBjxsplYT+BNCWzQBSBC7hCk5VgPx
6BvzlzBEWJYizpnT55Ta7zDV1tofP2RUt5Q6GT27Qm5fMlAj3a3LsmgeDLIPHhQd
RCo0kEc56X4vZyojaNUrmTzh6+Ljoj7ahEsW9fr8kfQvIlvuR1qjkuuCEUDU7kS/
iblwVkY1Lfrfm9mI82EYI287m28LBTP99ULk9KRhAQKBgQDpEjK0/OmsHSQfjiG7
PHQXrmIdMzaz+BYttiGV9Fx5hsdVPvihdjzzwZck2MkSg5ODMtEthb7uBareS3Nl
CG7a7brY8a/x5ZdnUPNXGykfix/oz557EENembKaWpsV8qiHM8vuADOWEvmqBTVt
C0iXrwvyxgy/GuNz9A9Tfyya3wKBgQDGNb9Pr903/JzJKFkT+4dGpAgE0a3eQsDm
HEJimbhNoOw79AyOHWbpV2f74kz0GdG2MjU3988lZ/VJ7FM0eyDkuBvv3c2YdKCm
A/5tprB/8PefdNJD0HuVm4BE2XDLV74DbOCgoqsFMC1BdeUVBAhSqmRNrYFQYRqj
DvqtDQiFKQKBgB5p6YQEnNmA0/3qJiywrtWIQ/VbgX/ql7pPUgKnaInTNJ/DH96x
9zI3yOleAJ8R3GX6c6FlGo0k4C8x2VUNzKl07DTzFOqT8zXgMmDjgnJDTV6r+RpF
/QSTOeM6f5JVn/hEog/kptamkz3EgDxChK6GgSClB3TIpXW0G2vh5IgxAoGBAIIl
WHDicMcKP4h1zcepKLHhksJXS2rdOfveIljLxpByUassG/JUq/YbRlPFy/Gb4m9X
mEoflQxirlTTr+6NypNjsDRX1197dOCNTsqA4POhLXauJkIQ6pTZfee3PrDF9CYb
n4LaTKEjeRO6bajW9QASkbnPa1Fz8SGP/FkUbbvBAoGAKIuvVLwht1A8C0BXaFrb
znZu3u90SB9TEcm2V9pU1ptiU6Q/CGlxm8UYvx1ahmxNYL6Ip/QNIFyb+HCqvIUf
Id3C+4LlLeXVBP0uBCX828zREhuQutq3kju2iOQfsOkwc1McS4WXk6tExXoVwkzl
2WYMu+GpSZLcti71L58tOf4=
-----END PRIVATE KEY-----

Some files were not shown because too many files have changed in this diff Show More