Compare commits
185
Commits
game-pages
...
0edaa9d8cc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0edaa9d8cc | ||
|
|
97d35f8fcb | ||
|
|
b565825cd9 | ||
|
|
07e8e68742 | ||
|
|
e470a9848d | ||
|
|
6fb3ed597b | ||
|
|
fd8d1730e8 | ||
|
|
cafa60b0f2 | ||
|
|
5dc01a358a | ||
|
|
4daaa8d102 | ||
|
|
996087ec4e | ||
|
|
c63af7becc | ||
|
|
552c71b718 | ||
|
|
c6227ea8c1 | ||
|
|
1c0ab4780b | ||
|
|
7f389fbbc2 | ||
|
|
ffa2b12786 | ||
|
|
51c542c7bb | ||
|
|
a290b75238 | ||
|
|
2b3c90d3fc | ||
|
|
5d9c113eea | ||
|
|
779ddcbccb | ||
|
|
6b1436dfe7 | ||
|
|
02780d8f04 | ||
|
|
9a52e6b32c | ||
|
|
7c14ab3331 | ||
|
|
6a04a0274a | ||
|
|
367ded6441 | ||
|
|
1f35784c52 | ||
|
|
b0b357f99b | ||
|
|
a00899e444 | ||
|
|
aba79251e0 | ||
|
|
2951a39204 | ||
|
|
2bda70b32b | ||
|
|
ebd2f68df6 | ||
|
|
2c8e568255 | ||
|
|
ba45e06972 | ||
|
|
2913b8d2a2 | ||
|
|
335697e520 | ||
|
|
daa37390d0 | ||
|
|
a9cb0fa57f | ||
|
|
2bfc2aca1a | ||
|
|
846cfbc44a | ||
|
|
207346d571 | ||
|
|
dfa75719d0 | ||
|
|
444f54b6c6 | ||
|
|
98cf48b29d | ||
|
|
f6df9f7ba6 | ||
|
|
fc93486367 | ||
|
|
98066118a6 | ||
|
|
1be07440e3 | ||
|
|
c28abef5b7 | ||
|
|
886208c5c0 | ||
|
|
2941cfca49 | ||
|
|
3c58e153dc | ||
|
|
3a7bc3ed49 | ||
|
|
aa667df889 | ||
|
|
c06c45cb52 | ||
|
|
51679d9a6a | ||
|
|
e8be7919ef | ||
|
|
cb2e945419 | ||
|
|
3984a33282 | ||
|
|
eee6c3a369 | ||
|
|
e6ba469ef9 | ||
|
|
f6a0d62017 | ||
|
|
6fd0b5d993 | ||
|
|
6db9d42852 | ||
|
|
1e644eb13b | ||
|
|
8554f04735 | ||
|
|
839113c356 | ||
|
|
7dbb738da8 | ||
|
|
ec34a1ddb9 | ||
|
|
703d2af3d8 | ||
|
|
5b03bd1d1c | ||
|
|
05f4f2c32f | ||
|
|
ddd2726687 | ||
|
|
3721abcc5d | ||
|
|
e76d0b0f07 | ||
|
|
b7693bd9d0 | ||
|
|
b4f6a531c9 | ||
|
|
c4e5139ec4 | ||
|
|
238705495e | ||
|
|
4a5d83ef53 | ||
|
|
0e12e7fa8f | ||
|
|
3924b42ce0 | ||
|
|
58c9c60ad2 | ||
|
|
b11c50c237 | ||
|
|
2316266b80 | ||
|
|
2a45ebb953 | ||
|
|
a1ff6df721 | ||
|
|
487019d360 | ||
|
|
c045922c5b | ||
|
|
aa56617e50 | ||
|
|
831603e04e | ||
|
|
ac57385a9d | ||
|
|
0d8335dd2c | ||
|
|
6c40288fd8 | ||
|
|
994bbafba2 | ||
|
|
a05adfd316 | ||
|
|
9205db6611 | ||
|
|
3b1d3a5aad | ||
|
|
8b3bf68954 | ||
|
|
74dadf102d | ||
|
|
74d5cd15b6 | ||
|
|
4e696af231 | ||
|
|
539a243353 | ||
|
|
eb3d36c99f | ||
|
|
8564257761 | ||
|
|
be01de83ed | ||
|
|
700160e198 | ||
|
|
96c2e4ca61 | ||
|
|
e5f959210a | ||
|
|
e14cf8457b | ||
|
|
2c970fa9e7 | ||
|
|
714496fa77 | ||
|
|
2b7e667bb3 | ||
|
|
b8c1fecea2 | ||
|
|
4f40c96ce5 | ||
|
|
4545572b65 | ||
|
|
1c27c093c7 | ||
|
|
6d8d30e91a | ||
|
|
5c8a5f0bf5 | ||
|
|
6b5c205a27 | ||
|
|
70f5aa785e | ||
|
|
6c96d45a04 | ||
|
|
8d4e08e4bc | ||
|
|
6fba1cbcf3 | ||
|
|
1512a3dde9 | ||
|
|
5f4e2acd27 | ||
|
|
203dfaa13b | ||
|
|
799858d52c | ||
|
|
ce79b77244 | ||
|
|
5f17c9b89f | ||
|
|
5335e62cfd | ||
|
|
04b1ff6243 | ||
|
|
84b7b1c64a | ||
|
|
b0af41c5d2 | ||
|
|
a94b8cbef3 | ||
|
|
3f33ec6fef | ||
|
|
ab80d4de83 | ||
|
|
b2817c5d8c | ||
|
|
c59d131b80 | ||
|
|
e8a6c9cc7a | ||
|
|
e84f2c274e | ||
|
|
d9bc8c352f | ||
|
|
ece3b05a9c | ||
|
|
4cf614a98c | ||
|
|
24f51bf38d | ||
|
|
0acf60760f | ||
|
|
342858fa0e | ||
|
|
3c9bd13cc2 | ||
|
|
eeda97cef8 | ||
|
|
5a6616484f | ||
|
|
f0bcc9fe06 | ||
|
|
550cc1b2ed | ||
|
|
b1910a63c3 | ||
|
|
60d52d7b5e | ||
|
|
0c7bbc2670 | ||
|
|
3e73cba942 | ||
|
|
6027dcb56c | ||
|
|
81b91e052c | ||
|
|
a5a895b67f | ||
|
|
28c663749c | ||
|
|
b063e17863 | ||
|
|
2008a379a8 | ||
|
|
c0fc0b8e6e | ||
|
|
928ab3cbfe | ||
|
|
4d021e7cf1 | ||
|
|
834f12c40f | ||
|
|
30aa73bf53 | ||
|
|
9d9de0fd0d | ||
|
|
979623b35e | ||
|
|
2984a6acb2 | ||
|
|
cdb469456f | ||
|
|
0ac2618f7d | ||
|
|
b6c09c267f | ||
|
|
229d55390d | ||
|
|
5eff66c24d | ||
|
|
7c86c919c7 | ||
|
|
cdd5bc3fd8 | ||
|
|
e69e794e27 | ||
|
|
173407cd26 | ||
|
|
90e5fd904f | ||
|
|
225c14124a | ||
|
|
a932161973 |
@@ -0,0 +1,25 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# In all environments, the following files are loaded if they exist,
|
||||||
|
# the latter taking precedence over the former:
|
||||||
|
#
|
||||||
|
# * .env contains default values for the environment variables needed by the app
|
||||||
|
# * .env.local uncommitted file with local overrides
|
||||||
|
# * .env.$APP_ENV committed environment-specific defaults
|
||||||
|
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||||
|
#
|
||||||
|
# Real environment variables win over .env files.
|
||||||
|
#
|
||||||
|
# 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=^.*$
|
||||||
|
###< 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}"
|
||||||
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
|
###> symfony/messenger ###
|
||||||
|
# Choose one of the transports below
|
||||||
|
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||||
|
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
||||||
|
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||||
|
###< symfony/messenger ###
|
||||||
|
|
||||||
|
###> 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
|
||||||
|
# Optional default sender (used by test command if --from not passed):
|
||||||
|
###< symfony/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
|
||||||
|
# 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 ###
|
||||||
|
|
||||||
|
###> 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 ###
|
||||||
+12
@@ -1,11 +1,19 @@
|
|||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
|
/.env
|
||||||
|
/.env.dev
|
||||||
|
/.env.prod
|
||||||
|
/.env.test
|
||||||
/.env.local
|
/.env.local
|
||||||
/.env.local.php
|
/.env.local.php
|
||||||
/.env.*.local
|
/.env.*.local
|
||||||
/config/secrets/prod/prod.decrypt.private.php
|
/config/secrets/prod/prod.decrypt.private.php
|
||||||
|
/config/reference.php
|
||||||
/public/bundles/
|
/public/bundles/
|
||||||
/var/
|
/var/
|
||||||
|
!/var/volumes/
|
||||||
|
/var/volumes/*
|
||||||
|
!/var/volumes/.gitignore
|
||||||
/vendor/
|
/vendor/
|
||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
@@ -27,3 +35,7 @@ yarn-error.log
|
|||||||
###< symfony/webpack-encore-bundle ###
|
###< symfony/webpack-encore-bundle ###
|
||||||
|
|
||||||
/.idea
|
/.idea
|
||||||
|
|
||||||
|
###> docker env ###
|
||||||
|
/docker/.env
|
||||||
|
###< docker env ###
|
||||||
|
|||||||
Generated
+4
-1
@@ -136,10 +136,13 @@
|
|||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/lcobucci/jwt" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/lcobucci/jwt" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure-bundle" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure-bundle" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/sendgrid-mailer" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/webpack-encore-bundle" />
|
<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/reset-password-bundle" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/verify-email-bundle" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/verify-email-bundle" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/karser/karser-recaptcha3-bundle" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mailgun-mailer" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php85" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/rate-limiter" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|||||||
Generated
+4
-1
@@ -146,13 +146,16 @@
|
|||||||
<path value="$PROJECT_DIR$/vendor/monolog/monolog" />
|
<path value="$PROJECT_DIR$/vendor/monolog/monolog" />
|
||||||
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
||||||
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/sendgrid-mailer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/webpack-encore-bundle" />
|
<path value="$PROJECT_DIR$/vendor/symfony/webpack-encore-bundle" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/mercure" />
|
<path value="$PROJECT_DIR$/vendor/symfony/mercure" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/mercure-bundle" />
|
<path value="$PROJECT_DIR$/vendor/symfony/mercure-bundle" />
|
||||||
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfonycasts/verify-email-bundle" />
|
<path value="$PROJECT_DIR$/vendor/symfonycasts/verify-email-bundle" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfonycasts/reset-password-bundle" />
|
<path value="$PROJECT_DIR$/vendor/symfonycasts/reset-password-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/karser/karser-recaptcha3-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/mailgun-mailer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/rate-limiter" />
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
|
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
|
||||||
|
|||||||
@@ -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`
|
6. Run tests: `vendor/bin/phpunit`
|
||||||
|
|
||||||
- With Docker:
|
- With Docker:
|
||||||
1. From `docker/`: `docker compose up -d`
|
1. `cd docker && docker compose up -d`
|
||||||
2. Install vendors inside the PHP container:
|
2. Install vendors inside the PHP container:
|
||||||
- `docker compose exec php bash`
|
- `docker compose exec php bash`
|
||||||
- `composer install`
|
- `composer install`
|
||||||
@@ -25,23 +25,25 @@ This repository contains a Symfony 7.3 (PHP >= 8.5.1) application for a collabor
|
|||||||
- `php bin/console doctrine:migrations:migrate -n`
|
- `php bin/console doctrine:migrations:migrate -n`
|
||||||
4. App is at http://localhost:8080
|
4. App is at http://localhost:8080
|
||||||
|
|
||||||
## Email (Mailpit in dev, SendGrid for prod)
|
## Email (Mailpit in dev, Mailgun for prod)
|
||||||
- Dev: a `mailer` service (Mailpit) runs in Docker.
|
- Dev: a `mailer` service (Mailpit) runs in Docker.
|
||||||
- SMTP DSN in `.env`: `MAILER_DSN=smtp://mailer:1025`
|
- SMTP DSN in `.env`: `MAILER_DSN=smtp://mailer:1025`
|
||||||
- Mailpit UI: http://localhost:8025
|
- Mailpit UI: http://localhost:8025 (or mapped port 8025)
|
||||||
- Send a test mail: `php bin/console app:mail:test you@example.com`
|
- Send a test mail: `php bin/console app:mail:test you@example.com`
|
||||||
- Staging/Prod: use SendGrid.
|
- Staging/Prod: use Mailgun.
|
||||||
- Require package (already in composer): `symfony/sendgrid-mailer`.
|
- Require package (already in composer): `symfony/mailgun-mailer`.
|
||||||
- Set environment variables (do NOT commit secrets):
|
- Set environment variables (do NOT commit secrets):
|
||||||
- `MAILER_DSN=sendgrid+api://%env(SENDGRID_API_KEY)%`
|
- `MAILER_DSN=mailgun+api://${MAILGUN_API_KEY}:${MAILGUN_DOMAIN}@default?region=eu`
|
||||||
- `SENDGRID_API_KEY=YOUR_REAL_KEY`
|
- `MAILGUN_API_KEY=YOUR_REAL_KEY`
|
||||||
|
- `MAILGUN_DOMAIN=YOUR_SENDING_DOMAIN` (e.g. `mg.escapepage.nl`)
|
||||||
- Optional: `MAILER_FROM=no-reply@your-domain.tld`
|
- 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):
|
- Alternatively via SMTP (no extra package):
|
||||||
- `MAILER_DSN="smtp://apikey:%env(SENDGRID_API_KEY)%@smtp.sendgrid.net:587?encryption=tls"`
|
- `MAILER_DSN="mailgun+smtp://USERNAME:PASSWORD@default?region=eu"`
|
||||||
|
|
||||||
Troubleshooting:
|
Troubleshooting:
|
||||||
- If emails don’t appear in dev, open Mailpit at http://localhost:8025 and verify messages.
|
- If emails don’t appear in dev, open Mailpit at http://localhost:8025 and verify messages.
|
||||||
- In prod, check logs for HTTP 2xx responses from SendGrid and verify sender domain is verified in SendGrid.
|
- In prod, check logs for HTTP 2xx responses from Mailgun and verify sender domain is verified (SPF/DKIM) in Mailgun.
|
||||||
|
|
||||||
## Frontend assets with Webpack Encore
|
## Frontend assets with Webpack Encore
|
||||||
We use Webpack Encore to build and minify JS/CSS from the `assets/` directory into `public/build/`.
|
We use Webpack Encore to build and minify JS/CSS from the `assets/` directory into `public/build/`.
|
||||||
@@ -81,9 +83,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 Server‑Sent Events (SSE).
|
We use a Mercure hub (Docker service) to push server updates to browsers via Server‑Sent Events (SSE).
|
||||||
|
|
||||||
Quick start (dev):
|
Quick start (dev):
|
||||||
1. Start Docker stack from `docker/`:
|
1. Start Docker stack:
|
||||||
```
|
```
|
||||||
docker compose up -d
|
cd docker && docker compose up -d
|
||||||
```
|
```
|
||||||
This starts `mercure` at http://localhost:8090 and the app at http://localhost:8080.
|
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:
|
2. Install PHP deps inside the PHP container if you haven't yet:
|
||||||
@@ -91,12 +93,6 @@ Quick start (dev):
|
|||||||
docker compose exec php bash
|
docker compose exec php bash
|
||||||
composer install
|
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.
|
You should see a console log like `[Mercure] Update received: { ... }` on the Game Hub page.
|
||||||
|
|
||||||
Configuration:
|
Configuration:
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { DataTable } from 'simple-datatables';
|
||||||
|
import 'simple-datatables/dist/style.css';
|
||||||
|
|
||||||
|
// Paginate/search/sort any admin table opted in via class="admin-datatable".
|
||||||
|
// Kept off tables that are admin-curated and stay small (games) or where row
|
||||||
|
// order is meaningful and must not be disturbed by sorting (hints).
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll('table.admin-datatable').forEach((table) => {
|
||||||
|
new DataTable(table, {
|
||||||
|
perPage: 25,
|
||||||
|
perPageSelect: [10, 25, 50, 100],
|
||||||
|
searchable: true,
|
||||||
|
sortable: true,
|
||||||
|
labels: {
|
||||||
|
placeholder: 'Search...',
|
||||||
|
perPage: '{select} entries per page',
|
||||||
|
noRows: 'No matching entries found',
|
||||||
|
info: 'Showing {start} to {end} of {rows} entries',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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));
|
||||||
|
});
|
||||||
|
});
|
||||||
+8
-3
@@ -1,6 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
* Welcome to your app's main JavaScript file!
|
* Welcome to your app's main JavaScript file!
|
||||||
*/
|
*/
|
||||||
import './styles/app.css';
|
import './styles/app.scss';
|
||||||
|
import 'bootstrap/js/dist/collapse';
|
||||||
console.log('This log comes from assets/app.js built by Webpack Encore! 🎉');
|
import 'bootstrap/js/dist/alert';
|
||||||
|
import 'bootstrap/js/dist/dropdown';
|
||||||
|
import './game-waiting';
|
||||||
|
import './game-lobby';
|
||||||
|
import './admin-session-tabs';
|
||||||
|
import './admin-datatables';
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
+333
-7
@@ -1,7 +1,16 @@
|
|||||||
/* Game1 entry point built with Webpack Encore */
|
/* Game1 entry point built with Webpack Encore */
|
||||||
import './styles/game1.css';
|
import './styles/game1.css';
|
||||||
|
|
||||||
function subscribeToMercure(mercurePublicUrl, topic) {
|
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) {
|
||||||
try {
|
try {
|
||||||
const url = mercurePublicUrl + '?topic=' + encodeURIComponent(topic);
|
const url = mercurePublicUrl + '?topic=' + encodeURIComponent(topic);
|
||||||
const es = new EventSource(url);
|
const es = new EventSource(url);
|
||||||
@@ -10,6 +19,35 @@ function subscribeToMercure(mercurePublicUrl, topic) {
|
|||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
console.log('[Mercure][game1] Update:', 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');
|
||||||
|
if(stillPlayingSound)
|
||||||
|
playSound();
|
||||||
|
console.log('[Mercure][game1] sequenceFinished status:', sequenceFinished);
|
||||||
|
if (sequenceFinished) {
|
||||||
|
flashRed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('[Mercure][game1] Raw event:', event.data);
|
console.log('[Mercure][game1] Raw event:', event.data);
|
||||||
}
|
}
|
||||||
@@ -25,6 +63,169 @@ function subscribeToMercure(mercurePublicUrl, topic) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
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 = {}) {
|
async function fetchJson(url, options = {}) {
|
||||||
const opts = { ...options };
|
const opts = { ...options };
|
||||||
const headers = new Headers(opts.headers || {});
|
const headers = new Headers(opts.headers || {});
|
||||||
@@ -61,8 +262,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// Look for config injected by Twig in the page
|
// Look for config injected by Twig in the page
|
||||||
const cfgEl = document.getElementById('mercure-config');
|
const cfgEl = document.getElementById('mercure-config');
|
||||||
|
|
||||||
// Prevent/warn on page reload
|
// Prevent/warn on page reload, except for our own win/lose redirects
|
||||||
window.addEventListener('beforeunload', (event) => {
|
window.addEventListener('beforeunload', (event) => {
|
||||||
|
if (navigatingAway) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Standard way to trigger the browser's confirmation dialog
|
// Standard way to trigger the browser's confirmation dialog
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// Included for compatibility with older browsers
|
// Included for compatibility with older browsers
|
||||||
@@ -76,15 +280,74 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
const mercurePublicUrl = cfgEl.dataset.mercurePublicUrl;
|
const mercurePublicUrl = cfgEl.dataset.mercurePublicUrl;
|
||||||
const topic = cfgEl.dataset.topic;
|
const topic = cfgEl.dataset.topic;
|
||||||
|
const screen = cfgEl.dataset.screen;
|
||||||
const apiPingUrl = cfgEl.dataset.apiPingUrl;
|
const apiPingUrl = cfgEl.dataset.apiPingUrl;
|
||||||
const apiEchoUrl = cfgEl.dataset.apiEchoUrl;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
if (mercurePublicUrl && topic) {
|
if (mercurePublicUrl && topic) {
|
||||||
subscribeToMercure(mercurePublicUrl, topic);
|
subscribeToMercure(mercurePublicUrl, topic, screen, wonUrl, lostUrl);
|
||||||
} else {
|
} else {
|
||||||
console.warn('[Mercure][game1] Missing data attributes on #mercure-config');
|
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
|
// Demo API calls
|
||||||
try {
|
try {
|
||||||
if (apiPingUrl) {
|
if (apiPingUrl) {
|
||||||
@@ -113,7 +376,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
let messages = [
|
let messages = [
|
||||||
['System initializing...', 500],
|
['System initializing...', 500],
|
||||||
['Connection established.', 200],
|
['Connection established.', 200],
|
||||||
['Welcome agent to the mainframe.', 1000],
|
['Welcome agent ' + screen + ' to the mainframe.', 1000],
|
||||||
['Scanning...', 3000],
|
['Scanning...', 3000],
|
||||||
['Virus detected.', 500],
|
['Virus detected.', 500],
|
||||||
['Starting Mainframe help modus...', 2000],
|
['Starting Mainframe help modus...', 2000],
|
||||||
@@ -125,16 +388,25 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
const printNextMessage = () => {
|
const printNextMessage = () => {
|
||||||
if (currentMessageIndex < messages.length) {
|
if (currentMessageIndex < messages.length) {
|
||||||
|
|
||||||
const msg = messages[currentMessageIndex];
|
const msg = messages[currentMessageIndex];
|
||||||
const msgEl = document.createElement('div');
|
const msgEl = document.createElement('div');
|
||||||
msgEl.className = 'message';
|
|
||||||
|
let extraClass = '';
|
||||||
|
if(msg[2])
|
||||||
|
extraClass = msg[2];
|
||||||
|
|
||||||
|
msgEl.className = 'message ' + extraClass;
|
||||||
msgEl.textContent = msg[0];
|
msgEl.textContent = msg[0];
|
||||||
msgEl.style.color = '#F00';
|
|
||||||
msgEl.style.marginBottom = '10px';
|
|
||||||
messageContainer.appendChild(msgEl);
|
messageContainer.appendChild(msgEl);
|
||||||
|
|
||||||
|
playSound();
|
||||||
|
|
||||||
currentMessageIndex++;
|
currentMessageIndex++;
|
||||||
setTimeout(printNextMessage, msg[1]);
|
setTimeout(printNextMessage, msg[1]);
|
||||||
|
if (sequenceFinished) {
|
||||||
|
flashRed();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// After it has printed a set of messages, it has to start a timer of 2 seconds
|
// 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...');
|
console.log('[Game1] All messages printed. Starting 2s timer to expand message-container height...');
|
||||||
@@ -143,11 +415,31 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
const inputField = document.getElementById('input-message');
|
const inputField = document.getElementById('input-message');
|
||||||
inputField.disabled = false;
|
inputField.disabled = false;
|
||||||
|
|
||||||
|
let lastCommand = '';
|
||||||
|
|
||||||
|
// Recall the last submitted command on ArrowUp, cursor at the end
|
||||||
|
inputField.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
inputField.value = lastCommand;
|
||||||
|
inputField.setSelectionRange(inputField.value.length, inputField.value.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Add event listener for Enter key
|
// Add event listener for Enter key
|
||||||
inputField.addEventListener('keypress', async (e) => {
|
inputField.addEventListener('keypress', async (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
|
stillPlayingSound = false;
|
||||||
|
sequenceFinished = false;
|
||||||
const message = inputField.value.trim();
|
const message = inputField.value.trim();
|
||||||
|
|
||||||
|
const msgEl = document.createElement('div');
|
||||||
|
msgEl.className = 'message';
|
||||||
|
msgEl.textContent = message;
|
||||||
|
messageContainer.appendChild(msgEl);
|
||||||
|
|
||||||
if (message && apiEchoUrl) {
|
if (message && apiEchoUrl) {
|
||||||
|
lastCommand = message;
|
||||||
inputField.value = '';
|
inputField.value = '';
|
||||||
try {
|
try {
|
||||||
const response = await fetchJson(apiEchoUrl, {
|
const response = await fetchJson(apiEchoUrl, {
|
||||||
@@ -155,6 +447,29 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
body: { message, ts: new Date().toISOString() },
|
body: { message, ts: new Date().toISOString() },
|
||||||
});
|
});
|
||||||
console.log('[API][game1] message sent →', response);
|
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));
|
||||||
|
}
|
||||||
|
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) {
|
} catch (err) {
|
||||||
console.error('[API][game1] Failed to send message:', err);
|
console.error('[API][game1] Failed to send message:', err);
|
||||||
}
|
}
|
||||||
@@ -163,6 +478,17 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log('[Game1] message-container height changed to 400vh and input enabled');
|
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);
|
}, 2000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# /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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# /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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
archive-node-04
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Debian GNU/Linux 12 \n \l
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Welcome to archive-node-04.
|
||||||
|
All connections are logged and monitored for internal review purposes.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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/*;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
passwd: files
|
||||||
|
group: files
|
||||||
|
shadow: files
|
||||||
|
hosts: files dns
|
||||||
|
networks: files
|
||||||
|
protocols: db files
|
||||||
|
services: db files
|
||||||
|
ethers: db files
|
||||||
|
rpc: db files
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
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"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
nameserver 1.1.1.1
|
||||||
|
nameserver 9.9.9.9
|
||||||
|
options edns0
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
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:::
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
Host *
|
||||||
|
SendEnv LANG LC_*
|
||||||
|
HashKnownHosts yes
|
||||||
|
GSSAPIAuthentication yes
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Port 22
|
||||||
|
PermitRootLogin no
|
||||||
|
PasswordAuthentication yes
|
||||||
|
PubkeyAuthentication yes
|
||||||
|
X11Forwarding no
|
||||||
|
PrintMotd no
|
||||||
|
Subsystem sftp /usr/lib/openssh/sftp-server
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Europe/Amsterdam
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
app:
|
||||||
|
name: internal-archive-sync
|
||||||
|
version: 2.3.1
|
||||||
|
log_level: info
|
||||||
|
port: 8080
|
||||||
|
database:
|
||||||
|
driver: sqlite
|
||||||
|
path: /opt/app/data.db
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
apt update
|
||||||
|
apt upgrade -y
|
||||||
|
systemctl restart nginx
|
||||||
|
df -h
|
||||||
|
journalctl -xe
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# ~/.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'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGZ8pQxT2mN0vRkLwYb6cJhU3sEoAeKdVmXpZq7tRnBs admin@archive-node-04
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
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]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[ OK ] Started Network Manager.
|
||||||
|
[ OK ] Started OpenSSH server daemon.
|
||||||
|
[ OK ] Started Nginx HTTP server.
|
||||||
|
[ OK ] Reached target Multi-User System.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
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'
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[ 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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
/* Pregame (commandline tutorial) entry point built with Webpack Encore */
|
||||||
|
import './styles/pregame.css';
|
||||||
|
|
||||||
|
function appendMessage(container, text, extraClass = '') {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = ('message ' + extraClass).trim();
|
||||||
|
el.textContent = text;
|
||||||
|
container.appendChild(el);
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dockBackButton(inputField) {
|
||||||
|
const backButton = document.getElementById('back-button');
|
||||||
|
const inputContainer = document.getElementById('input');
|
||||||
|
if (!backButton || !inputContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputField.style.display = 'none';
|
||||||
|
backButton.classList.add('docked');
|
||||||
|
inputContainer.appendChild(backButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
console.log('Pregame bundle loaded');
|
||||||
|
|
||||||
|
const messageContainer = document.getElementById('message-container');
|
||||||
|
const inputField = document.getElementById('input-message');
|
||||||
|
|
||||||
|
if (!messageContainer || !inputField) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bootMessages = [
|
||||||
|
['Booting tutorial terminal...', 500],
|
||||||
|
['Loading command line basics...', 800],
|
||||||
|
['"Welcome agent. Lets go through the basics together.', 2000],
|
||||||
|
['Let us start with the help command. Type help in the terminal (the input field at the bottom of the screen and press enter.', 500],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Same command help text as GameResponseService::getHelpCommand(), minus "scan".
|
||||||
|
const helpMessages = [
|
||||||
|
'Help function:',
|
||||||
|
'',
|
||||||
|
'pwd',
|
||||||
|
' This message will let you know what your current location is.',
|
||||||
|
' It will show you the folder you are in so you can continue navigating the server.',
|
||||||
|
' USAGE: pwd',
|
||||||
|
'',
|
||||||
|
'cd',
|
||||||
|
' Use cd to move to a different directory.',
|
||||||
|
' You can go into a folder by using cd {foldername}, or a folder up by using "cd ..".',
|
||||||
|
' Using cd / moves you to the root directory.',
|
||||||
|
' USAGE: cd {directory}',
|
||||||
|
'',
|
||||||
|
'ls',
|
||||||
|
' To show all the files in the current directory, use ls.',
|
||||||
|
' This will print the full list of directories and files of the current location on your screen.',
|
||||||
|
' USAGE: ls',
|
||||||
|
'',
|
||||||
|
'cat',
|
||||||
|
' To read a file, use cat {filename}.',
|
||||||
|
' This will print the full content of the file on the screen.',
|
||||||
|
' USAGE: cat {filename}',
|
||||||
|
'',
|
||||||
|
'rm',
|
||||||
|
' Use rm to delete a file.',
|
||||||
|
' Be careful with this command. It can not be undone and we do not want to lose any valuable data.',
|
||||||
|
' USAGE: rm {filename}',
|
||||||
|
'',
|
||||||
|
'sudo',
|
||||||
|
' If you do not have enough rights to execute a command, you can use sudo to execute it as root.',
|
||||||
|
' This is only possible for verified users. To verify yourself, use the /verify command.',
|
||||||
|
' USAGE: sudo {command}',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Narrative shown after the help output finishes, introducing the next step.
|
||||||
|
// Each entry is [delayBeforeShowing, text, cssClass].
|
||||||
|
const helpFollowUp = [
|
||||||
|
[2000, 'Well done, you now have a complete overview of the terminal commands this game uses.', 'message-mainframe'],
|
||||||
|
[500, 'Not all commands will be available immediately. But lets go through them one by one.', 'message-mainframe'],
|
||||||
|
[1000, 'Lets start with the command pwd. Enter this in the terminal (input field) now and press enter.', 'message-mainframe'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const pwdFollowUp = [
|
||||||
|
[1000, 'You now know the location you are in on the terminal. /var/home/agent.', 'message-mainframe'],
|
||||||
|
[500, 'You can change directories with the cd command.', 'message-mainframe'],
|
||||||
|
[500, "If you type 'cd ..' in the terminal, you will go one directory up to /var/home", 'message-mainframe'],
|
||||||
|
[500, 'Try this now.', 'message-mainframe'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const cdFollowUp = [
|
||||||
|
[1000, 'Good. Please check the directory you are in now.', 'message-mainframe'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const pwd2FollowUp = [
|
||||||
|
[1000, 'As you can see, you are now in the /var/home directory', 'message-mainframe'],
|
||||||
|
[500, 'You can move between directories like this.', 'message-mainframe'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const wrongPwdMessage = "That is not the command which gives you the location you are in. You can check your current folder with the pwd command.";
|
||||||
|
|
||||||
|
const lsIntro = [
|
||||||
|
[1000, 'Now lets see what is actually in this directory. The ls command lists all files and folders in your current location.', 'message-mainframe'],
|
||||||
|
[500, 'Type ls now.', 'message-mainframe'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const lsListing = [
|
||||||
|
'agent,dir',
|
||||||
|
'peter,dir',
|
||||||
|
'james,dir',
|
||||||
|
'william,dir',
|
||||||
|
'system,dir',
|
||||||
|
];
|
||||||
|
|
||||||
|
const lsFollowUp = [
|
||||||
|
[1000, 'You can see several folders here, including your own: agent.', 'message-mainframe'],
|
||||||
|
[500, 'Lets go back to your own directory. Use cd agent to enter it.', 'message-mainframe'],
|
||||||
|
[250, 'Remember, cd /var/home/agent also works to get there directly from anywhere on the server.', 'message-mainframe'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const cdAgentFollowUp = [
|
||||||
|
[1000, 'Lets check if you now really are in your own home folder.', 'message-mainframe'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const verifyAgentFollowUp = [
|
||||||
|
[1000, "Well done, you're right back where you started.", 'message-mainframe'],
|
||||||
|
[500, 'Now lets move to the next command: cat.', 'message-mainframe'],
|
||||||
|
[500, 'First, lets see what is inside this folder.', 'message-mainframe'],
|
||||||
|
[500, 'Type ls to check the content of /var/home/agent.', 'message-mainframe'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const lsAgentListing = [
|
||||||
|
'HelloWorld.txt,file',
|
||||||
|
'notes.txt,file',
|
||||||
|
'schedule.txt,file',
|
||||||
|
'contacts.db,file',
|
||||||
|
'keycard.dat,file',
|
||||||
|
'backup.zip,file',
|
||||||
|
];
|
||||||
|
|
||||||
|
const lsAgentFinalListing = [
|
||||||
|
'notes.txt,file',
|
||||||
|
'schedule.txt,file',
|
||||||
|
'contacts.db,file',
|
||||||
|
'keycard.dat,file',
|
||||||
|
'backup.zip,file',
|
||||||
|
];
|
||||||
|
|
||||||
|
const lsAgentFollowUp = [
|
||||||
|
[1000, 'There it is: HelloWorld.txt, right there in the list.', 'message-mainframe'],
|
||||||
|
[500, 'Lets read what is inside. Type cat HelloWorld.txt to open the file.', 'message-mainframe'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const helloWorldContent = [
|
||||||
|
'Day 1 at the agency. My mentor says curiosity keeps you sharp -- and alive.',
|
||||||
|
"- I keep a spare set of shoelaces in my desk. You'd be surprised how handy that is.",
|
||||||
|
'- The coffee machine on this floor is cursed. Never trust it before 9am.',
|
||||||
|
'- Jared Williams owes me a favor after the incident with the elevator. He should not have forgotten that.',
|
||||||
|
'',
|
||||||
|
'If you are reading this, agent: welcome to the team. Stay sharp out there.',
|
||||||
|
];
|
||||||
|
|
||||||
|
const catFollowUp = [
|
||||||
|
[1000, 'Nice work. cat is perfect for quickly reading any file on the server.', 'message-mainframe'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const rmIntro = [
|
||||||
|
[1000, "There's one more command left in the basics: rm.", 'message-mainframe'],
|
||||||
|
[500, 'This command permanently deletes a file, so use it with caution.', 'message-mainframe'],
|
||||||
|
[500, 'Try removing HelloWorld.txt now. Type rm HelloWorld.txt.', 'message-mainframe'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const rmDeniedMessages = [
|
||||||
|
'Permission denied: HelloWorld.txt is protected against removal.',
|
||||||
|
'You need sudo rights to remove this file.',
|
||||||
|
];
|
||||||
|
|
||||||
|
const rmFollowUp = [
|
||||||
|
[1000, 'sudo can technically be used for any command, at any moment.', 'message-mainframe'],
|
||||||
|
[500, 'But from a security standpoint, it is better not to use it unless it is truly necessary.', 'message-mainframe'],
|
||||||
|
[500, 'Only reach for sudo in extreme cases -- like this one, where a file is actively protected.', 'message-mainframe'],
|
||||||
|
[500, 'Try it now. Type sudo rm HelloWorld.txt.', 'message-mainframe'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const sudoRmFollowUp = [
|
||||||
|
[1000, 'Well done. Since you used sudo, the protection was bypassed and the file was deleted.', 'message-mainframe'],
|
||||||
|
[500, 'Lets check the folder once more to confirm it is really gone. Type ls now.', 'message-mainframe'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const tutorialEndFollowUp = [
|
||||||
|
[1000, 'As you can see, HelloWorld.txt is no longer in the list. It has been permanently removed.', 'message-mainframe'],
|
||||||
|
[500, 'That concludes the basics of the terminal, agent. You are ready for the real mission.', 'message-mainframe'],
|
||||||
|
[1000, 'Please be aware more commands can exist to help this mission succeed. These commands will show up in the help command. Good luck!', 'message-mainframe']
|
||||||
|
];
|
||||||
|
|
||||||
|
function playSequence(items, container, onDone) {
|
||||||
|
let i = 0;
|
||||||
|
const step = () => {
|
||||||
|
if (i >= items.length) {
|
||||||
|
if (onDone) onDone();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [delay, text, cls] = items[i];
|
||||||
|
i++;
|
||||||
|
setTimeout(() => {
|
||||||
|
appendMessage(container, text, cls);
|
||||||
|
step();
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
step();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracks which command the tutorial is currently guiding the player towards.
|
||||||
|
let tutorialStage = 'help';
|
||||||
|
|
||||||
|
let currentMessageIndex = 0;
|
||||||
|
|
||||||
|
const printNextMessage = () => {
|
||||||
|
if (currentMessageIndex < bootMessages.length) {
|
||||||
|
const [text, delay] = bootMessages[currentMessageIndex];
|
||||||
|
appendMessage(messageContainer, text, 'message-mainframe');
|
||||||
|
currentMessageIndex++;
|
||||||
|
setTimeout(printNextMessage, delay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputField.disabled = false;
|
||||||
|
inputField.focus();
|
||||||
|
|
||||||
|
inputField.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key !== 'Enter') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = inputField.value.trim();
|
||||||
|
inputField.value = '';
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
appendMessage(messageContainer, value);
|
||||||
|
|
||||||
|
if (value.toLowerCase() === 'help' || value.toLowerCase() === '/help') {
|
||||||
|
// No extraClass -> default "message" color, same as the real game's help output.
|
||||||
|
helpMessages.forEach((line) => appendMessage(messageContainer, line));
|
||||||
|
|
||||||
|
if (tutorialStage === 'help') {
|
||||||
|
playSequence(helpFollowUp, messageContainer, () => {
|
||||||
|
tutorialStage = 'pwd';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tutorialStage === 'pwd' && value.toLowerCase() === 'pwd') {
|
||||||
|
appendMessage(messageContainer, '/var/home/agent');
|
||||||
|
playSequence(pwdFollowUp, messageContainer, () => {
|
||||||
|
tutorialStage = 'cd';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tutorialStage === 'cd' && value.toLowerCase() === 'cd ..') {
|
||||||
|
playSequence(cdFollowUp, messageContainer, () => {
|
||||||
|
tutorialStage = 'pwd2';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tutorialStage === 'pwd2') {
|
||||||
|
if (value.toLowerCase() === 'pwd') {
|
||||||
|
appendMessage(messageContainer, '/var/home');
|
||||||
|
playSequence(pwd2FollowUp, messageContainer, () => {
|
||||||
|
playSequence(lsIntro, messageContainer, () => {
|
||||||
|
tutorialStage = 'ls';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
appendMessage(messageContainer, wrongPwdMessage, 'message-hint');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tutorialStage === 'ls' && value.toLowerCase() === 'ls') {
|
||||||
|
lsListing.forEach((name) => appendMessage(messageContainer, name));
|
||||||
|
playSequence(lsFollowUp, messageContainer, () => {
|
||||||
|
tutorialStage = 'cd-agent';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
tutorialStage === 'cd-agent' &&
|
||||||
|
(value.toLowerCase() === 'cd agent' || value.toLowerCase() === 'cd /var/home/agent')
|
||||||
|
) {
|
||||||
|
playSequence(cdAgentFollowUp, messageContainer, () => {
|
||||||
|
tutorialStage = 'verify-agent';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tutorialStage === 'verify-agent' && value.toLowerCase() === 'pwd') {
|
||||||
|
appendMessage(messageContainer, '/var/home/agent');
|
||||||
|
playSequence(verifyAgentFollowUp, messageContainer, () => {
|
||||||
|
tutorialStage = 'ls-agent';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tutorialStage === 'ls-agent' && value.toLowerCase() === 'ls') {
|
||||||
|
lsAgentListing.forEach((name) => appendMessage(messageContainer, name));
|
||||||
|
playSequence(lsAgentFollowUp, messageContainer, () => {
|
||||||
|
tutorialStage = 'cat-file';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tutorialStage === 'cat-file' && value.toLowerCase() === 'cat helloworld.txt') {
|
||||||
|
helloWorldContent.forEach((line) => appendMessage(messageContainer, line));
|
||||||
|
playSequence(catFollowUp, messageContainer, () => {
|
||||||
|
playSequence(rmIntro, messageContainer, () => {
|
||||||
|
tutorialStage = 'rm';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tutorialStage === 'rm' && value.toLowerCase() === 'rm helloworld.txt') {
|
||||||
|
rmDeniedMessages.forEach((line) => appendMessage(messageContainer, line, 'message-hint'));
|
||||||
|
playSequence(rmFollowUp, messageContainer, () => {
|
||||||
|
tutorialStage = 'sudo-rm';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tutorialStage === 'sudo-rm' && value.toLowerCase() === 'sudo rm helloworld.txt') {
|
||||||
|
appendMessage(messageContainer, 'File removed: HelloWorld.txt');
|
||||||
|
playSequence(sudoRmFollowUp, messageContainer, () => {
|
||||||
|
tutorialStage = 'ls-confirm';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tutorialStage === 'ls-confirm' && value.toLowerCase() === 'ls') {
|
||||||
|
lsAgentFinalListing.forEach((name) => appendMessage(messageContainer, name));
|
||||||
|
playSequence(tutorialEndFollowUp, messageContainer, () => {
|
||||||
|
tutorialStage = 'complete';
|
||||||
|
inputField.disabled = true;
|
||||||
|
dockBackButton(inputField);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder response until the rest of the tutorial content is defined.
|
||||||
|
appendMessage(messageContainer, 'Command not recognized yet.', 'message-hint');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
printNextMessage();
|
||||||
|
});
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
body {
|
|
||||||
background-color: skyblue;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
+88
-9
@@ -29,8 +29,11 @@ body {
|
|||||||
|
|
||||||
div#game-timer {
|
div#game-timer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 0;
|
||||||
left: 20px;
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #000;
|
||||||
color: #F00;
|
color: #F00;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
@@ -38,13 +41,67 @@ div#game-timer {
|
|||||||
|
|
||||||
div#message-container {
|
div#message-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
padding-top: 60px; /* Space for fixed timer */
|
padding-top: 80px; /* Space for fixed timer */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
min-height: calc(100vh - 100px); /* Fill most of the viewport initially */
|
min-height: calc(100vh - 100px); /* Fill most of the viewport initially */
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 20px;
|
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 {
|
div#input {
|
||||||
@@ -53,11 +110,33 @@ div#input {
|
|||||||
|
|
||||||
input#input-message {
|
input#input-message {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 6px 10px;
|
||||||
background: #000;
|
background: #111;
|
||||||
border: 1px solid #F00;
|
border: 1px solid #A00000;
|
||||||
color: #F00;
|
color: #C0C0C0;
|
||||||
font-size: 18px;
|
font-size: 14px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-family: monospace;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/* Styles for the pregame (commandline tutorial) terminal, mirroring game1's look */
|
||||||
|
|
||||||
|
html::-webkit-scrollbar,
|
||||||
|
body::-webkit-scrollbar {
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar-track,
|
||||||
|
body::-webkit-scrollbar-track {
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar-thumb,
|
||||||
|
body::-webkit-scrollbar-thumb {
|
||||||
|
background: #F00;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #000;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: monospace;
|
||||||
|
margin: 0;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #F00 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#message-container {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
min-height: calc(100vh - 80px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.message {
|
||||||
|
color: #C0C0C0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.35;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.message-mainframe {
|
||||||
|
color: #0F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.message-virus {
|
||||||
|
color: #F00;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.message-hint {
|
||||||
|
color: #FF0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
a#back-button {
|
||||||
|
position: fixed;
|
||||||
|
top: 16px;
|
||||||
|
left: 16px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: #111;
|
||||||
|
border: 1px solid #A00000;
|
||||||
|
color: #C0C0C0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: monospace;
|
||||||
|
text-decoration: none;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
a#back-button:hover {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
a#back-button.docked {
|
||||||
|
position: static;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
services:
|
|
||||||
###> symfony/mercure-bundle ###
|
|
||||||
mercure:
|
|
||||||
ports:
|
|
||||||
- "80"
|
|
||||||
###< symfony/mercure-bundle ###
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
|
|
||||||
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 ###
|
|
||||||
+51
-49
@@ -7,48 +7,50 @@
|
|||||||
"php": ">=8.2",
|
"php": ">=8.2",
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"doctrine/dbal": "^3",
|
"doctrine/dbal": "^4.4.4",
|
||||||
"doctrine/doctrine-bundle": "^2.16",
|
"doctrine/doctrine-bundle": "^2.18.3",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.4",
|
"doctrine/doctrine-migrations-bundle": "^3.7",
|
||||||
"doctrine/orm": "^3.5",
|
"doctrine/orm": "^3.6.7",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"karser/karser-recaptcha3-bundle": "^0.3.0",
|
||||||
"phpstan/phpdoc-parser": "^2.3",
|
"phpdocumentor/reflection-docblock": "^6.0.3",
|
||||||
"symfony/asset": "7.3.*",
|
"phpstan/phpdoc-parser": "^2.3.2",
|
||||||
"symfony/asset-mapper": "7.3.*",
|
"symfony/asset": "7.4.*",
|
||||||
"symfony/console": "7.3.*",
|
"symfony/asset-mapper": "7.4.*",
|
||||||
"symfony/doctrine-messenger": "7.3.*",
|
"symfony/console": "7.4.*",
|
||||||
"symfony/dotenv": "7.3.*",
|
"symfony/doctrine-messenger": "7.4.*",
|
||||||
"symfony/expression-language": "7.3.*",
|
"symfony/dotenv": "7.4.*",
|
||||||
"symfony/flex": "^2",
|
"symfony/expression-language": "7.4.*",
|
||||||
"symfony/form": "7.3.*",
|
"symfony/flex": "^2.11",
|
||||||
"symfony/framework-bundle": "7.3.*",
|
"symfony/form": "7.4.*",
|
||||||
"symfony/http-client": "7.3.*",
|
"symfony/framework-bundle": "7.4.*",
|
||||||
"symfony/intl": "7.3.*",
|
"symfony/http-client": "7.4.*",
|
||||||
"symfony/mailer": "7.3.*",
|
"symfony/intl": "7.4.*",
|
||||||
"symfony/mercure-bundle": "^0.3",
|
"symfony/mailer": "7.4.*",
|
||||||
"symfony/mime": "7.3.*",
|
"symfony/mailgun-mailer": "7.4.*",
|
||||||
"symfony/monolog-bundle": "^3.0",
|
"symfony/mercure-bundle": "^0.5.0",
|
||||||
"symfony/notifier": "7.3.*",
|
"symfony/mime": "7.4.*",
|
||||||
"symfony/process": "7.3.*",
|
"symfony/monolog-bundle": "^4.0.2",
|
||||||
"symfony/property-access": "7.3.*",
|
"symfony/notifier": "7.4.*",
|
||||||
"symfony/property-info": "7.3.*",
|
"symfony/process": "7.4.*",
|
||||||
"symfony/runtime": "7.3.*",
|
"symfony/property-access": "7.4.*",
|
||||||
"symfony/security-bundle": "7.3.*",
|
"symfony/property-info": "7.4.*",
|
||||||
"symfony/sendgrid-mailer": "7.3.*",
|
"symfony/rate-limiter": "7.4.*",
|
||||||
"symfony/serializer": "7.3.*",
|
"symfony/runtime": "7.4.*",
|
||||||
"symfony/stimulus-bundle": "^2.30",
|
"symfony/security-bundle": "7.4.*",
|
||||||
"symfony/string": "7.3.*",
|
"symfony/serializer": "7.4.*",
|
||||||
"symfony/translation": "7.3.*",
|
"symfony/stimulus-bundle": "^2.36",
|
||||||
"symfony/twig-bundle": "7.3.*",
|
"symfony/string": "7.4.*",
|
||||||
"symfony/ux-turbo": "^2.30",
|
"symfony/translation": "7.4.*",
|
||||||
"symfony/validator": "7.3.*",
|
"symfony/twig-bundle": "7.4.*",
|
||||||
"symfony/web-link": "7.3.*",
|
"symfony/ux-turbo": "^2.36",
|
||||||
"symfony/webpack-encore-bundle": "^2.1",
|
"symfony/validator": "7.4.*",
|
||||||
"symfony/yaml": "7.3.*",
|
"symfony/web-link": "7.4.*",
|
||||||
"symfonycasts/reset-password-bundle": "^1.24",
|
"symfony/webpack-encore-bundle": "^2.4.1",
|
||||||
|
"symfony/yaml": "7.4.*",
|
||||||
|
"symfonycasts/reset-password-bundle": "^1.25",
|
||||||
"symfonycasts/verify-email-bundle": "^1.18",
|
"symfonycasts/verify-email-bundle": "^1.18",
|
||||||
"twig/extra-bundle": "^2.12|^3.0",
|
"twig/extra-bundle": "^2.12|^3.24",
|
||||||
"twig/twig": "^2.12|^3.0"
|
"twig/twig": "^2.12|^3.28.0"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
@@ -98,16 +100,16 @@
|
|||||||
"extra": {
|
"extra": {
|
||||||
"symfony": {
|
"symfony": {
|
||||||
"allow-contrib": false,
|
"allow-contrib": false,
|
||||||
"require": "7.3.*"
|
"require": "7.4.*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^11.5",
|
"phpunit/phpunit": "^11.5.55",
|
||||||
"symfony/browser-kit": "7.3.*",
|
"symfony/browser-kit": "7.4.*",
|
||||||
"symfony/css-selector": "7.3.*",
|
"symfony/css-selector": "7.4.*",
|
||||||
"symfony/debug-bundle": "7.3.*",
|
"symfony/debug-bundle": "7.4.*",
|
||||||
"symfony/maker-bundle": "^1.0",
|
"symfony/maker-bundle": "^1.67",
|
||||||
"symfony/stopwatch": "7.3.*",
|
"symfony/stopwatch": "7.4.*",
|
||||||
"symfony/web-profiler-bundle": "7.3.*"
|
"symfony/web-profiler-bundle": "7.4.*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1547
-1243
File diff suppressed because it is too large
Load Diff
@@ -17,4 +17,5 @@ return [
|
|||||||
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
|
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
|
||||||
SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true],
|
SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true],
|
||||||
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
|
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
|
||||||
|
Karser\Recaptcha3Bundle\KarserRecaptcha3Bundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
# Enable stateless CSRF protection for forms and logins/logouts
|
# Enable stateless CSRF protection for forms and logins/logouts
|
||||||
framework:
|
framework:
|
||||||
form:
|
# form:
|
||||||
csrf_protection:
|
# csrf_protection:
|
||||||
token_id: submit
|
# token_id: submit
|
||||||
|
# csrf_protection:
|
||||||
csrf_protection:
|
# stateless_token_ids:
|
||||||
stateless_token_ids:
|
# - submit
|
||||||
- submit
|
# - authenticate
|
||||||
- authenticate
|
# - logout
|
||||||
- logout
|
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
doctrine:
|
doctrine:
|
||||||
dbal:
|
dbal:
|
||||||
url: '%env(resolve:DATABASE_URL)%'
|
# 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)%'
|
||||||
|
|
||||||
# IMPORTANT: You MUST configure your server version,
|
# IMPORTANT: You MUST configure your server version,
|
||||||
# either here or in the DATABASE_URL env var (see .env file)
|
# either here or in the DATABASE_URL env var (see .env file)
|
||||||
#server_version: '16'
|
#server_version: '16'
|
||||||
|
|
||||||
profiling_collect_backtrace: '%kernel.debug%'
|
profiling_collect_backtrace: '%kernel.debug%'
|
||||||
use_savepoints: true
|
|
||||||
orm:
|
orm:
|
||||||
auto_generate_proxy_classes: true
|
auto_generate_proxy_classes: true
|
||||||
enable_lazy_ghost_objects: true
|
enable_lazy_ghost_objects: true
|
||||||
report_fields_where_declared: true
|
|
||||||
validate_xml_mapping: true
|
validate_xml_mapping: true
|
||||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||||
auto_mapping: true
|
auto_mapping: true
|
||||||
|
|||||||
@@ -8,7 +8,27 @@ framework:
|
|||||||
fallbacks: ['en', 'nl']
|
fallbacks: ['en', 'nl']
|
||||||
|
|
||||||
# Note that the session will be started ONLY if you read or write from it.
|
# Note that the session will be started ONLY if you read or write from it.
|
||||||
session: true
|
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%'
|
||||||
|
|
||||||
#esi: true
|
#esi: true
|
||||||
#fragments: true
|
#fragments: true
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
karser_recaptcha3:
|
||||||
|
site_key: '%env(RECAPTCHA3_KEY)%'
|
||||||
|
secret_key: '%env(RECAPTCHA3_SECRET)%'
|
||||||
|
score_threshold: 0.5
|
||||||
|
enabled: true
|
||||||
@@ -3,4 +3,6 @@ mercure:
|
|||||||
default:
|
default:
|
||||||
url: '%env(MERCURE_URL)%'
|
url: '%env(MERCURE_URL)%'
|
||||||
public_url: '%env(MERCURE_PUBLIC_URL)%'
|
public_url: '%env(MERCURE_PUBLIC_URL)%'
|
||||||
jwt: '%env(MERCURE_JWT_SECRET)%'
|
jwt:
|
||||||
|
secret: '%env(MERCURE_JWT_SECRET)%'
|
||||||
|
publish: ['*']
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ when@prod:
|
|||||||
excluded_http_codes: [404, 405]
|
excluded_http_codes: [404, 405]
|
||||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||||
nested:
|
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
|
type: stream
|
||||||
path: php://stderr
|
path: php://stderr
|
||||||
level: debug
|
level: debug
|
||||||
@@ -56,7 +64,14 @@ when@prod:
|
|||||||
process_psr_3_messages: false
|
process_psr_3_messages: false
|
||||||
channels: ["!event", "!doctrine"]
|
channels: ["!event", "!doctrine"]
|
||||||
deprecation:
|
deprecation:
|
||||||
type: stream
|
type: group
|
||||||
channels: [deprecation]
|
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
|
path: php://stderr
|
||||||
formatter: monolog.formatter.json
|
formatter: monolog.formatter.json
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ framework:
|
|||||||
notifier:
|
notifier:
|
||||||
chatter_transports:
|
chatter_transports:
|
||||||
texter_transports:␍
|
texter_transports:␍
|
||||||
sendgrid: '%env(MAILER_DSN)%'
|
|
||||||
channel_policy:
|
channel_policy:
|
||||||
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
|
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
|
||||||
urgent: ['email']
|
urgent: ['email']
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ framework:
|
|||||||
router:
|
router:
|
||||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
# 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
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||||
#default_uri: http://localhost
|
default_uri: '%env(SITE_BASE_URL)%'
|
||||||
|
|
||||||
when@prod:
|
when@prod:
|
||||||
framework:
|
framework:
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ security:
|
|||||||
enable_csrf: true
|
enable_csrf: true
|
||||||
username_parameter: username
|
username_parameter: username
|
||||||
password_parameter: password
|
password_parameter: password
|
||||||
|
login_throttling:
|
||||||
|
max_attempts: 5
|
||||||
|
interval: '15 minutes'
|
||||||
logout:
|
logout:
|
||||||
path: app_logout
|
path: app_logout
|
||||||
# where to redirect after logout
|
# where to redirect after logout
|
||||||
@@ -30,6 +33,7 @@ security:
|
|||||||
# Easy way to control access for large sections of your site
|
# Easy way to control access for large sections of your site
|
||||||
# Note: Only the *first* access control that matches will be used
|
# Note: Only the *first* access control that matches will be used
|
||||||
access_control:
|
access_control:
|
||||||
|
- { path: ^/, roles: PUBLIC_ACCESS, requires_channel: https }
|
||||||
# - { path: ^/admin, roles: ROLE_ADMIN }
|
# - { path: ^/admin, roles: ROLE_ADMIN }
|
||||||
# - { path: ^/profile, roles: ROLE_USER }
|
# - { path: ^/profile, roles: ROLE_USER }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
karser_recaptcha3:
|
||||||
|
enabled: false
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
twig:
|
twig:
|
||||||
|
form_themes: ['bootstrap_5_layout.html.twig']
|
||||||
globals:
|
globals:
|
||||||
mercure_public_url: '%env(MERCURE_PUBLIC_URL)%'
|
mercure_public_url: '%env(MERCURE_PUBLIC_URL)%'
|
||||||
mercure_topic_base: '%env(MERCURE_TOPIC_BASE)%'
|
mercure_topic_base: '%env(MERCURE_TOPIC_BASE)%'
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
# 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
|
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||||
parameters:
|
parameters:
|
||||||
|
mailer_from: '%env(MAILER_FROM)%'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
@@ -16,5 +17,9 @@ services:
|
|||||||
App\:
|
App\:
|
||||||
resource: '../src/'
|
resource: '../src/'
|
||||||
|
|
||||||
|
App\Game\Service\GameResponseService:
|
||||||
|
arguments:
|
||||||
|
$projectDir: '%kernel.project_dir%'
|
||||||
|
|
||||||
# add more service definitions when explicit configuration is needed
|
# add more service definitions when explicit configuration is needed
|
||||||
# please note that last definitions always *replace* previous ones
|
# please note that last definitions always *replace* previous ones
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ Use this index to quickly locate files and directories during development and in
|
|||||||
|
|
||||||
## Top-Level
|
## Top-Level
|
||||||
- docker/compose.yaml / docker/compose.override.yaml — Docker services.
|
- docker/compose.yaml / docker/compose.override.yaml — Docker services.
|
||||||
- docker/ — Docker build contexts and configs (php Dockerfile, nginx vhost, compose files).
|
- docker/ — Docker build contexts and configs (php Dockerfile, nginx vhost).
|
||||||
- composer.json / composer.lock — Dependencies and scripts.
|
- composer.json / composer.lock — Dependencies and scripts.
|
||||||
- importmap.php — Importmap configuration for JS dependencies.
|
- importmap.php — Importmap configuration for JS dependencies.
|
||||||
- phpunit.dist.xml — PHPUnit configuration.
|
- phpunit.dist.xml — PHPUnit configuration.
|
||||||
|
|||||||
+15
-9
@@ -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)
|
- mailer (dev only via compose.override.yaml): Mailpit (SMTP/UI)
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
- Docker and Docker Compose (v2)
|
- Docker and Docker Compose (docker compose)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -21,36 +21,42 @@ App will be served at http://localhost:8080
|
|||||||
|
|
||||||
Alternatively (manual):
|
Alternatively (manual):
|
||||||
```
|
```
|
||||||
docker compose -f docker/compose.yaml -f docker/compose.override.yaml up -d --build
|
cd docker
|
||||||
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2) Install dependencies
|
### 2) Install dependencies
|
||||||
The setup script already runs composer install. To run manually:
|
The setup script already runs composer install. To run manually:
|
||||||
```
|
```
|
||||||
docker compose -f docker/compose.yaml -f docker/compose.override.yaml exec php composer install
|
cd docker
|
||||||
|
docker compose exec php composer install
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3) Prepare DB
|
### 3) Prepare DB
|
||||||
The setup script already prepares the DB. To run manually:
|
The setup script already prepares the DB. To run manually:
|
||||||
```
|
```
|
||||||
docker compose -f docker/compose.yaml -f docker/compose.override.yaml exec php php bin/console doctrine:database:create --if-not-exists
|
cd docker
|
||||||
docker compose -f docker/compose.yaml -f docker/compose.override.yaml exec php php bin/console doctrine:migrations:migrate -n
|
docker compose exec php php bin/console doctrine:database:create --if-not-exists
|
||||||
|
docker compose exec php php bin/console doctrine:migrations:migrate -n
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4) Run tests
|
### 4) Run tests
|
||||||
```
|
```
|
||||||
docker compose -f docker/compose.yaml -f docker/compose.override.yaml exec php vendor/bin/phpunit
|
cd docker
|
||||||
|
docker compose exec php vendor/bin/phpunit
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5) Logs
|
### 5) Logs
|
||||||
```
|
```
|
||||||
docker compose -f docker/compose.yaml -f docker/compose.override.yaml logs -f nginx
|
cd docker
|
||||||
docker compose -f docker/compose.yaml -f docker/compose.override.yaml logs -f php
|
docker compose logs -f nginx
|
||||||
|
docker compose logs -f php
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6) Stop
|
### 6) Stop
|
||||||
```
|
```
|
||||||
docker compose -f docker/compose.yaml -f docker/compose.override.yaml down
|
cd docker
|
||||||
|
docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|||||||
+14
-11
@@ -1,9 +1,9 @@
|
|||||||
# Email Delivery: Dev Mailcatcher & Production SendGrid
|
# Email Delivery: Dev Mailcatcher & Production Mailgun
|
||||||
|
|
||||||
This application uses Symfony Mailer. We separate development and production delivery:
|
This application uses Symfony Mailer. We separate development and production delivery:
|
||||||
|
|
||||||
- Development: Mailpit (mailcatcher) via SMTP in Docker.
|
- Development: Mailpit (mailcatcher) via SMTP in Docker.
|
||||||
- Production: SendGrid via API transport.
|
- Production: Mailgun via API transport.
|
||||||
|
|
||||||
## Development (Mailpit)
|
## Development (Mailpit)
|
||||||
|
|
||||||
@@ -18,28 +18,31 @@ MAILER_DSN=smtp://mailer:1025
|
|||||||
```
|
```
|
||||||
|
|
||||||
- Usage:
|
- Usage:
|
||||||
1. Start stack: `docker compose up -d`
|
1. Start stack: `docker-compose up -d`
|
||||||
2. Send an email from the app.
|
2. Send an email from the app.
|
||||||
3. Open http://localhost:8025 to view captured emails.
|
3. Open http://localhost:8025 to view captured emails.
|
||||||
|
|
||||||
## Production (SendGrid)
|
## Production (Mailgun)
|
||||||
|
|
||||||
Use the SendGrid API transport. Do not commit secrets.
|
Use the Mailgun API transport (`symfony/mailgun-mailer` bridge). Do not commit secrets.
|
||||||
|
|
||||||
- Example configuration is in `.env.prod`:
|
- Example configuration is in `.env.prod`:
|
||||||
|
|
||||||
```
|
```
|
||||||
MAILER_DSN=sendgrid+api://%env(resolve:SENDGRID_API_KEY)%@default
|
MAILER_DSN=mailgun+api://${MAILGUN_API_KEY}:${MAILGUN_DOMAIN}@default?region=eu
|
||||||
```
|
```
|
||||||
|
|
||||||
- Provide `SENDGRID_API_KEY` via:
|
- `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.
|
||||||
- Real environment variable on the server/container, or
|
- Provide `MAILGUN_API_KEY` and `MAILGUN_DOMAIN` via:
|
||||||
- Symfony secrets: `php bin/console secrets:set SENDGRID_API_KEY` (and dump for prod), or
|
- Real environment variables on the server/container, or
|
||||||
|
- Symfony secrets: `php bin/console secrets:set MAILGUN_API_KEY` (and dump for prod), or
|
||||||
- Orchestration secret stores (e.g., Docker/K8s).
|
- 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
|
### Notes
|
||||||
- No Mailpit container is defined in the base `compose.yaml`, only in `compose.override.yaml`. This ensures it is used in development only.
|
- 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:
|
- 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`.
|
- 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 SendGrid SMTP instead of API, a DSN example:
|
- If you need to use Mailgun SMTP instead of API, a DSN example:
|
||||||
`smtp://apikey:YOUR_SENDGRID_API_KEY@smtp.sendgrid.net:587`.
|
`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.
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# Test / staging environment (`test.escapepage.com`)
|
||||||
|
|
||||||
|
Runs a second copy of the full Docker stack on the same server as production,
|
||||||
|
behind the same Nginx Proxy Manager (NPM). Production is untouched: with no
|
||||||
|
`docker/.env` overrides, `compose.yaml` behaves exactly as before.
|
||||||
|
|
||||||
|
## How isolation works
|
||||||
|
|
||||||
|
`docker/compose.yaml` derives everything instance-specific from `docker/.env`:
|
||||||
|
|
||||||
|
| Concern | Mechanism | prod (no overrides) | test |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Container names | `${STACK_NAME:-escapepage}-*` | `escapepage-php` … | `escapepage-test-php` … |
|
||||||
|
| Compose project (networks, labels) | `COMPOSE_PROJECT_NAME` | `docker` (unchanged) | `escapepage-test` |
|
||||||
|
| Published ports | `${NGINX_HTTP_PORT:-8080}` etc. | `0.0.0.0:8080/8443/3306/8090/8025` | `127.0.0.1:8081/8444/3307/8091/8026` |
|
||||||
|
| Database files | bind mount `../var/volumes/db` | per checkout | per checkout |
|
||||||
|
| Web reachability | `nginx` joined to external `nginx_default` | via NPM | via NPM |
|
||||||
|
|
||||||
|
`STACK_NAME` is a plain variable (not the Compose-managed `COMPOSE_PROJECT_NAME`),
|
||||||
|
so its `:-escapepage` default is reliable and **production needs no `docker/.env`
|
||||||
|
change** to keep its `escapepage-*` container names.
|
||||||
|
|
||||||
|
`nginx`, `mercure` and `mailer` all sit on the external `nginx_default` network,
|
||||||
|
so NPM forwards straight to `escapepage-test-nginx` / `-mercure` by name — no
|
||||||
|
public host port needed.
|
||||||
|
|
||||||
|
## Production checkout — optional
|
||||||
|
|
||||||
|
Nothing is required. Two optional tidy-ups, each needing a `docker compose up -d`
|
||||||
|
to recreate the affected container:
|
||||||
|
|
||||||
|
- The `nginx` service now also attaches to `nginx_default`. If you'd rather have
|
||||||
|
NPM forward to `escapepage-nginx` by name instead of `host:8080`, recreate it
|
||||||
|
and repoint the NPM proxy host. Otherwise the published `8080/8443` still work
|
||||||
|
as before and you can ignore this.
|
||||||
|
- Add `STACK_NAME=escapepage` and `COMPOSE_PROJECT_NAME=escapepage` to the
|
||||||
|
production `docker/.env` to move it off the implicit `docker` project name.
|
||||||
|
Cosmetic; do it only if you want the two stacks named symmetrically.
|
||||||
|
|
||||||
|
## Standing up the test stack
|
||||||
|
|
||||||
|
### 1. DNS
|
||||||
|
`test.escapepage.com` → server IP. (`mercure-test.escapepage.com` too if you want
|
||||||
|
live game features.) On Cloudflare, DNS-only ("grey cloud") keeps it low-profile.
|
||||||
|
|
||||||
|
### 2. Separate checkout
|
||||||
|
```bash
|
||||||
|
git clone <repo> /opt/escapepage-test
|
||||||
|
cd /opt/escapepage-test
|
||||||
|
git checkout <branch-to-test>
|
||||||
|
```
|
||||||
|
Use a **separate clone**, not `git worktree` — the Docker build context is the
|
||||||
|
checkout directory and full isolation avoids surprises.
|
||||||
|
|
||||||
|
### 3. Compose env
|
||||||
|
```bash
|
||||||
|
cp docker/.env.test.example docker/.env
|
||||||
|
# edit: real passwords, fresh MERCURE_JWT_SECRET (openssl rand -hex 32), reCAPTCHA keys
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Symfony env overrides
|
||||||
|
Create `/opt/escapepage-test/.env.local` in the checkout (git-ignored):
|
||||||
|
```
|
||||||
|
APP_SECRET=<openssl rand -hex 16>
|
||||||
|
# Behind NPM the forwarded client IP lands from a Docker-private range; trust them
|
||||||
|
# all on staging so URL generation / HTTPS detection work.
|
||||||
|
TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||||
|
```
|
||||||
|
`APP_ENV=prod`, `DATABASE_URL`, `SITE_BASE_URL`, `MERCURE_*` and `MAILER_DSN` are
|
||||||
|
already supplied to the containers by `docker/.env`.
|
||||||
|
|
||||||
|
### 5. Build & start
|
||||||
|
```bash
|
||||||
|
./docker/setup.test.sh
|
||||||
|
```
|
||||||
|
The test-specific script (not `setup.sh`): it refuses to run unless
|
||||||
|
`COMPOSE_PROJECT_NAME` in `docker/.env` is a non-prod value, scopes every compose
|
||||||
|
call to that project, runs the DB/cache/console steps as `www-data`, and repairs
|
||||||
|
`var/cache` / `var/log` / `var/sessions` ownership at the end (bare `docker exec`
|
||||||
|
runs as root, which otherwise leaves php-fpm unable to read its own cache — a
|
||||||
|
silent 500). It never touches `var/volumes/db`.
|
||||||
|
|
||||||
|
Builds `escapepage-test-*` images, starts the containers, creates + migrates
|
||||||
|
`escapepage_test`, builds assets. Re-run any time; `--no-build` skips the rebuild.
|
||||||
|
|
||||||
|
### 6. Nginx Proxy Manager — proxy host
|
||||||
|
- Domain: `test.escapepage.com`
|
||||||
|
- Forward to: `escapepage-test-nginx`, port **443**, scheme **https**
|
||||||
|
(the app nginx force-redirects 80→443 and serves a self-signed cert; leave
|
||||||
|
"Verify SSL" off — same arrangement as production)
|
||||||
|
- SSL: request a Let's Encrypt cert, Force SSL, HTTP/2
|
||||||
|
- Optional: add response header `X-Robots-Tag: noindex`
|
||||||
|
|
||||||
|
If you want live game screens, add a second proxy host
|
||||||
|
`mercure-test.escapepage.com` → `escapepage-test-mercure` port `80` (http).
|
||||||
|
|
||||||
|
### 7. Restrict access to your IP — NPM Access List
|
||||||
|
A host firewall on 80/443 can't help: prod and test share those ports on NPM.
|
||||||
|
Restrict at the proxy instead.
|
||||||
|
|
||||||
|
- **NPM → Access Lists → Add Access List**
|
||||||
|
- Name: e.g. `staging-allowlist`
|
||||||
|
- Authorisation: leave empty
|
||||||
|
- Access: `Allow <your.public.ip>` then a final `Deny all`
|
||||||
|
- Satisfy: **Any**
|
||||||
|
- Edit the `test.escapepage.com` proxy host → **Access List** → select it.
|
||||||
|
- Do the same on `mercure-test.escapepage.com` if you added it.
|
||||||
|
|
||||||
|
Everyone else gets `403` at the proxy. Update the IP in one place when it changes.
|
||||||
|
|
||||||
|
Defence in depth (optional): in `/opt/escapepage-test/docker/nginx/default.conf`,
|
||||||
|
inside the `server { listen 443 ... }` block:
|
||||||
|
```nginx
|
||||||
|
set_real_ip_from 172.16.0.0/12; # NPM's docker network
|
||||||
|
real_ip_header X-Forwarded-For;
|
||||||
|
allow <your.public.ip>;
|
||||||
|
deny all;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploying a new version to test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/sites/escapepage-test
|
||||||
|
git fetch && git checkout <branch> && git pull
|
||||||
|
./docker/setup.test.sh --no-build # composer install, migrate, build assets, fix perms
|
||||||
|
```
|
||||||
|
|
||||||
|
`--no-build` skips the image rebuild; drop it if the Dockerfile changed.
|
||||||
|
|
||||||
|
## Restarting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./docker/restart.test.sh # down + up, keeps the DB and images
|
||||||
|
./docker/restart.test.sh --build # also rebuild images
|
||||||
|
./docker/restart.test.sh --fresh-db # also wipe var/volumes/db and re-init MySQL
|
||||||
|
```
|
||||||
|
|
||||||
|
## Safety notes
|
||||||
|
|
||||||
|
- Use the **`*.test.sh`** scripts on this checkout, not `setup.sh` / `restart.sh`.
|
||||||
|
Both refuse to run unless `docker/.env` names a non-prod
|
||||||
|
`COMPOSE_PROJECT_NAME`, and both scope every action to that project — they
|
||||||
|
can't reach the production stack.
|
||||||
|
- `restart.test.sh` **keeps the database** by default (you loaded prod data into
|
||||||
|
it); `--fresh-db` is the only thing that wipes it. It never runs a host-wide
|
||||||
|
`docker system prune` / `docker builder prune`, and it only repairs ownership
|
||||||
|
of `var/cache` / `var/log` / `var/sessions` — **never `var/volumes/db`**
|
||||||
|
(chowning the MySQL data dir is what corrupted it earlier this build).
|
||||||
|
- The test `docker/.env` uses its own `DB_NAME` and passwords so a config slip
|
||||||
|
can't reach the production database.
|
||||||
|
- `MAILER_DSN=smtp://mailer:1026` keeps staging mail inside Mailpit instead of
|
||||||
|
sending through Mailgun.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# docker/.env for a TEST / STAGING stack (e.g. test.escapepage.com)
|
||||||
|
# =============================================================================
|
||||||
|
# Copy this to docker/.env inside the *test* checkout and fill in the blanks.
|
||||||
|
# docker/.env is git-ignored, so it never leaves the server.
|
||||||
|
#
|
||||||
|
# Compose reads this file automatically. Anything unique per stack is derived
|
||||||
|
# from COMPOSE_PROJECT_NAME + the *_PORT vars below, so the same compose.yaml
|
||||||
|
# serves both production and this copy.
|
||||||
|
#
|
||||||
|
# Two config layers, don't mix them up:
|
||||||
|
# - THIS file -> consumed by `docker compose` (container names, ports, and the
|
||||||
|
# runtime env it injects into the php containers).
|
||||||
|
# - <checkout>/.env(.local) -> consumed by Symfony itself (APP_SECRET,
|
||||||
|
# TRUSTED_PROXIES, messenger DSN, CLI database access, ...).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
## --- Instance identity -------------------------------------------------------
|
||||||
|
# Both must be unique on the host.
|
||||||
|
# STACK_NAME -> prefix for every container_name (escapepage-test-php …)
|
||||||
|
# COMPOSE_PROJECT_NAME -> compose project: isolates networks, volumes and the
|
||||||
|
# labels that `docker compose down` / *.test.sh act on.
|
||||||
|
# Must be a non-prod value or setup.test.sh / restart.test.sh
|
||||||
|
# refuse to run.
|
||||||
|
STACK_NAME=escapepage-test
|
||||||
|
COMPOSE_PROJECT_NAME=escapepage-test
|
||||||
|
|
||||||
|
## --- Published host ports ---------------------------------------------------
|
||||||
|
# Bound to localhost only: the sole public entrypoint is Nginx Proxy Manager,
|
||||||
|
# which reaches the containers over the shared `nginx_default` network by name.
|
||||||
|
# Pick ports that don't clash with the production stack (8080/8443/3306/8090/8025/1025).
|
||||||
|
NGINX_HTTP_PORT=127.0.0.1:8081
|
||||||
|
NGINX_HTTPS_PORT=127.0.0.1:8444
|
||||||
|
DB_HOST_PORT=127.0.0.1:3307
|
||||||
|
MERCURE_HTTP_PORT=127.0.0.1:8091
|
||||||
|
MAILPIT_UI_PORT=127.0.0.1:8026
|
||||||
|
MAILPIT_SMTP_PORT=127.0.0.1:1026
|
||||||
|
|
||||||
|
## --- PHP image build ------------------------------------------------------
|
||||||
|
USER_ID=1000
|
||||||
|
GROUP_ID=1000
|
||||||
|
|
||||||
|
## --- Symfony runtime (injected into php / php-worker / php-cron) ------------
|
||||||
|
APP_ENV=prod
|
||||||
|
SITE_BASE_URL=https://test.escapepage.com
|
||||||
|
|
||||||
|
# Staging should NOT send real mail. Point at the bundled Mailpit and read it
|
||||||
|
# at http://127.0.0.1:8026 on the server (or via an NPM host if you expose it).
|
||||||
|
MAILER_DSN=smtp://mailer:1026
|
||||||
|
MAILER_FROM=mailer@test.escapepage.com
|
||||||
|
|
||||||
|
## --- Database -------------------------------------------------------------
|
||||||
|
# `database` is the compose *service* name and resolves inside this stack's
|
||||||
|
# own network - keep it as-is. Use its own name + fresh credentials so a mistake
|
||||||
|
# here can never point at the production database.
|
||||||
|
DB_NAME=escapepage_test
|
||||||
|
DB_USER=escapepage
|
||||||
|
DB_PASSWORD=CHANGE_ME_test_db_password
|
||||||
|
MYSQL_ROOT_PASSWORD=CHANGE_ME_test_root_password
|
||||||
|
DATABASE_URL=pdo_mysql://escapepage:CHANGE_ME_test_db_password@database:3306/escapepage_test?serverVersion=8.0.32&charset=utf8mb4
|
||||||
|
|
||||||
|
## --- Mercure -----------------------------------------------------------------
|
||||||
|
# Internal hub URL (service name, stays the same). Public URL + CORS must be the
|
||||||
|
# test domain. Add a `mercure-test.escapepage.com` proxy host in NPM ->
|
||||||
|
# <project>-mercure:80.
|
||||||
|
MERCURE_URL=http://mercure/.well-known/mercure
|
||||||
|
MERCURE_PUBLIC_URL=https://mercure-test.escapepage.com/.well-known/mercure
|
||||||
|
MERCURE_JWT_SECRET=CHANGE_ME_generate_with_openssl_rand_hex_32
|
||||||
|
MERCURE_CORS_ALLOWED_ORIGINS="https://test.escapepage.com"
|
||||||
|
MERCURE_TOPIC_BASE=https://test.escapepage.com
|
||||||
|
|
||||||
|
## --- reCAPTCHA v3 ----------------------------------------------------------
|
||||||
|
# Register test.escapepage.com in the reCAPTCHA admin console and paste its keys,
|
||||||
|
# or leave the placeholders and expect the contact / "suggest a room" forms to
|
||||||
|
# fail captcha validation on staging.
|
||||||
|
RECAPTCHA3_KEY=CHANGE_ME_or_reuse_prod_if_domain_added
|
||||||
|
RECAPTCHA3_SECRET=CHANGE_ME_or_reuse_prod_if_domain_added
|
||||||
@@ -1,26 +1,38 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
php:
|
php:
|
||||||
environment:
|
environment:
|
||||||
XDEBUG_MODE: off
|
XDEBUG_MODE: "off"
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
depends_on:
|
depends_on:
|
||||||
- mailer
|
- mailer
|
||||||
|
# networks:
|
||||||
|
# backend:
|
||||||
|
# ipv4_address: 172.23.0.10
|
||||||
|
|
||||||
###> doctrine/doctrine-bundle ###
|
###> doctrine/doctrine-bundle ###
|
||||||
database:
|
###< doctrine/doctrine-bundle ###
|
||||||
ports:
|
|
||||||
- "3306"
|
|
||||||
###< doctrine/doctrine-bundle ###
|
|
||||||
|
|
||||||
###> symfony/mailer ###
|
###> symfony/mailer ###
|
||||||
mailer:
|
mailer:
|
||||||
image: axllent/mailpit
|
image: axllent/mailpit
|
||||||
ports:
|
ports:
|
||||||
- "1025:1025"
|
- "${MAILPIT_SMTP_PORT:-1025}:1025"
|
||||||
- "8025:8025"
|
- "${MAILPIT_UI_PORT:-8025}:8025"
|
||||||
environment:
|
environment:
|
||||||
MP_SMTP_AUTH_ACCEPT_ANY: 1
|
MP_SMTP_AUTH_ACCEPT_ANY: 1
|
||||||
MP_SMTP_AUTH_ALLOW_INSECURE: 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 ###
|
###< symfony/mailer ###
|
||||||
|
|||||||
+129
-42
@@ -1,113 +1,200 @@
|
|||||||
|
|
||||||
version: '3.7'
|
version: '3.7'
|
||||||
|
|
||||||
|
# This stack can run more than once on the same host (e.g. production + a
|
||||||
|
# test.escapepage.com staging copy). Everything that must be unique per instance
|
||||||
|
# comes from docker/.env:
|
||||||
|
# STACK_NAME -> container name prefix (default: escapepage)
|
||||||
|
# COMPOSE_PROJECT_NAME -> compose project / network namespace
|
||||||
|
# NGINX_HTTP_PORT etc. -> published host ports
|
||||||
|
# With no docker/.env overrides it behaves exactly as before: containers
|
||||||
|
# escapepage-*, ports 8080/8443/3306/8090/8025.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
php:
|
php:
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: docker/php/Dockerfile
|
dockerfile: docker/php/Dockerfile
|
||||||
container_name: escapepage-php
|
args:
|
||||||
|
USER_ID: ${USER_ID}
|
||||||
|
GROUP_ID: ${GROUP_ID}
|
||||||
|
container_name: ${STACK_NAME:-escapepage}-php
|
||||||
volumes:
|
volumes:
|
||||||
- ../:/var/www/html:delegated
|
- ../:/var/www/html:delegated
|
||||||
|
- /etc/hosts:/etc/hosts:ro
|
||||||
environment:
|
environment:
|
||||||
APP_ENV: dev
|
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:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
- mercure
|
- mercure
|
||||||
networks:
|
# networks:
|
||||||
- backend
|
# backend:
|
||||||
|
# ipv4_address: 172.23.0.10
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
php-worker:
|
php-worker:
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: docker/php/Dockerfile
|
dockerfile: docker/php/Dockerfile
|
||||||
container_name: escapepage-php-worker
|
args:
|
||||||
|
USER_ID: ${USER_ID}
|
||||||
|
GROUP_ID: ${GROUP_ID}
|
||||||
|
container_name: ${STACK_NAME:-escapepage}-php-worker
|
||||||
volumes:
|
volumes:
|
||||||
- ../:/var/www/html:delegated
|
- ../:/var/www/html:delegated
|
||||||
|
- /etc/hosts:/etc/hosts:ro
|
||||||
environment:
|
environment:
|
||||||
APP_ENV: dev
|
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:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
- mercure
|
- mercure
|
||||||
command: ["php", "bin/console", "messenger:consume", "async", "-vv"]
|
command: ["php", "bin/console", "messenger:consume", "async", "-vv"]
|
||||||
networks:
|
# networks:
|
||||||
- backend
|
# 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: ${STACK_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
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:1.29.4-alpine
|
image: nginx:1.29.4-alpine
|
||||||
container_name: escapepage-nginx
|
container_name: ${STACK_NAME:-escapepage}-nginx
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "${NGINX_HTTP_PORT:-8080}:80"
|
||||||
|
- "${NGINX_HTTPS_PORT:-8443}:443"
|
||||||
volumes:
|
volumes:
|
||||||
- ../:/var/www/html:ro
|
- ../:/var/www/html:ro
|
||||||
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||||
|
- /etc/hosts:/etc/hosts:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- php
|
- php
|
||||||
|
# Joined to the Nginx Proxy Manager network so NPM can forward straight to
|
||||||
|
# "<project>-nginx" without going back out to a published host port.
|
||||||
networks:
|
networks:
|
||||||
- backend
|
- default
|
||||||
|
- nginx_proxy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
mailer:
|
mailer:
|
||||||
image: axllent/mailpit:latest
|
image: axllent/mailpit:latest
|
||||||
container_name: escapepage-mailer
|
container_name: ${STACK_NAME:-escapepage}-mailer
|
||||||
ports:
|
ports:
|
||||||
- "8025:8025"
|
- "${MAILPIT_UI_PORT:-8025}:8025"
|
||||||
|
volumes:
|
||||||
|
- /etc/hosts:/etc/hosts:ro
|
||||||
networks:
|
networks:
|
||||||
- backend
|
- default
|
||||||
|
- nginx_proxy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
mercure:
|
mercure:
|
||||||
image: dunglas/mercure:v0.21
|
image: dunglas/mercure:v0.21
|
||||||
container_name: escapepage-mercure
|
container_name: ${STACK_NAME:-escapepage}-mercure
|
||||||
environment:
|
environment:
|
||||||
SERVER_NAME: ":80"
|
SERVER_NAME: "http://:80"
|
||||||
MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_SECRET:-!ChangeThisMercureJWT!}
|
MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_SECRET}
|
||||||
MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_JWT_SECRET:-!ChangeThisMercureJWT!}
|
MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_JWT_SECRET}
|
||||||
MERCURE_CORS_ALLOWED_ORIGINS: http://localhost:8080
|
MERCURE_CORS_ALLOWED_ORIGINS: ${MERCURE_CORS_ALLOWED_ORIGINS}
|
||||||
MERCURE_PUBLISH_ALLOWED_ORIGINS: http://localhost:8080
|
MERCURE_PUBLISH_ALLOWED_ORIGINS: ${MERCURE_CORS_ALLOWED_ORIGINS}
|
||||||
MERCURE_EXTRA_DIRECTIVES: |
|
MERCURE_EXTRA_DIRECTIVES: |
|
||||||
cors_origins http://localhost:8080
|
cors_origins ${MERCURE_CORS_ALLOWED_ORIGINS}
|
||||||
# Allow anonymous subscribers in dev only
|
publish_origins ${MERCURE_CORS_ALLOWED_ORIGINS}
|
||||||
anonymous
|
anonymous
|
||||||
ports:
|
ports:
|
||||||
- "8090:80"
|
- "${MERCURE_HTTP_PORT:-8090}:80"
|
||||||
|
volumes:
|
||||||
|
- /etc/hosts:/etc/hosts:ro
|
||||||
networks:
|
networks:
|
||||||
- backend
|
- default
|
||||||
|
- nginx_proxy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
###> doctrine/doctrine-bundle ###
|
###> doctrine/doctrine-bundle ###
|
||||||
database:
|
database:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
container_name: escapepage-db
|
container_name: ${STACK_NAME:-escapepage}-db
|
||||||
environment:
|
environment:
|
||||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-app}
|
MYSQL_DATABASE: ${DB_NAME}
|
||||||
MYSQL_USER: ${MYSQL_USER:-app}
|
MYSQL_USER: ${DB_USER}
|
||||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-!ChangeMe!}
|
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root}"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
command: ["--default-authentication-plugin=mysql_native_password", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"]
|
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"]
|
||||||
volumes:
|
volumes:
|
||||||
- database_data:/var/lib/mysql:rw
|
- ../var/volumes/db:/var/lib/mysql:rw
|
||||||
|
- ./mysql/init:/docker-entrypoint-initdb.d:ro
|
||||||
|
- /etc/hosts:/etc/hosts:ro
|
||||||
# Uncomment the two lines below if you need to access MySQL from your host (workbench, etc.)
|
# Uncomment the two lines below if you need to access MySQL from your host (workbench, etc.)
|
||||||
# ports:
|
ports:
|
||||||
# - "3306:3306"
|
- "${DB_HOST_PORT:-3306}:3306"
|
||||||
networks:
|
# networks:
|
||||||
- backend
|
# backend:
|
||||||
|
# ipv4_address: 172.23.0.15
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
###< doctrine/doctrine-bundle ###
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
volumes:
|
|
||||||
###> doctrine/doctrine-bundle ###
|
###> doctrine/doctrine-bundle ###
|
||||||
database_data:
|
|
||||||
###< doctrine/doctrine-bundle ###
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
backend:
|
nginx_proxy:
|
||||||
driver: bridge
|
external: true
|
||||||
|
name: nginx_default
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- 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;
|
||||||
@@ -1,6 +1,18 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
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;
|
root /var/www/html/public;
|
||||||
index index.php index.html;
|
index index.php index.html;
|
||||||
@@ -18,6 +30,14 @@ server {
|
|||||||
fastcgi_param DOCUMENT_ROOT $realpath_root;
|
fastcgi_param DOCUMENT_ROOT $realpath_root;
|
||||||
fastcgi_pass php:9000;
|
fastcgi_pass php:9000;
|
||||||
fastcgi_read_timeout 120;
|
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 {
|
location ~ /\.ht {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-----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-----
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
-----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-----
|
||||||
+47
-3
@@ -6,15 +6,33 @@ RUN apk add --no-cache \
|
|||||||
git \
|
git \
|
||||||
icu-dev \
|
icu-dev \
|
||||||
libzip-dev \
|
libzip-dev \
|
||||||
|
libxml2-dev \
|
||||||
oniguruma-dev \
|
oniguruma-dev \
|
||||||
g++ \
|
g++ \
|
||||||
make \
|
make \
|
||||||
nodejs \
|
nodejs \
|
||||||
npm
|
npm \
|
||||||
|
shadow \
|
||||||
|
logrotate
|
||||||
|
|
||||||
|
# Install PHP extension installer
|
||||||
|
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
|
||||||
|
|
||||||
# Install PHP extensions
|
# Install PHP extensions
|
||||||
RUN docker-php-ext-configure intl \
|
RUN install-php-extensions \
|
||||||
&& docker-php-ext-install -j$(nproc) intl pdo pdo_mysql opcache zip
|
intl \
|
||||||
|
pdo_mysql \
|
||||||
|
opcache \
|
||||||
|
zip \
|
||||||
|
tokenizer \
|
||||||
|
ctype \
|
||||||
|
iconv \
|
||||||
|
mbstring \
|
||||||
|
dom \
|
||||||
|
xml \
|
||||||
|
simplexml \
|
||||||
|
xmlreader \
|
||||||
|
xmlwriter
|
||||||
|
|
||||||
# Install composer
|
# Install composer
|
||||||
ENV COMPOSER_ALLOW_SUPERUSER=1 \
|
ENV COMPOSER_ALLOW_SUPERUSER=1 \
|
||||||
@@ -24,7 +42,33 @@ COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
|||||||
# Configure PHP
|
# Configure PHP
|
||||||
COPY docker/php/php.ini $PHP_INI_DIR/conf.d/zz-custom.ini
|
COPY docker/php/php.ini $PHP_INI_DIR/conf.d/zz-custom.ini
|
||||||
|
|
||||||
|
# Cron daemon (BusyBox's built-in crond) for the php-cron container.
|
||||||
|
# Harmless for the php/php-worker containers too, since they never invoke crond.
|
||||||
|
COPY docker/php/crontab /etc/crontabs/root
|
||||||
|
RUN chmod 0600 /etc/crontabs/root
|
||||||
|
|
||||||
|
# Log rotation for the cron hint-check and PHP error logs: 25MB per file, kept for 3 months.
|
||||||
|
COPY docker/php/logrotate/cron-hints.conf /etc/logrotate.d/cron-hints
|
||||||
|
COPY docker/php/logrotate/php-logs.conf /etc/logrotate.d/php-logs
|
||||||
|
RUN chmod 0644 /etc/logrotate.d/cron-hints /etc/logrotate.d/php-logs
|
||||||
|
|
||||||
|
# Adjust www-data UID/GID to match host user (default 1000)
|
||||||
|
ARG USER_ID=1000
|
||||||
|
ARG GROUP_ID=1000
|
||||||
|
|
||||||
|
RUN if [ ${USER_ID:-0} -ne 0 ] && [ ${GROUP_ID:-0} -ne 0 ]; then \
|
||||||
|
userdel -f www-data &&\
|
||||||
|
if getent group www-data ; then groupdel www-data; fi &&\
|
||||||
|
groupadd -g ${GROUP_ID} www-data &&\
|
||||||
|
useradd -l -u ${USER_ID} -g www-data www-data &&\
|
||||||
|
install -d -m 0755 -o www-data -g www-data /home/www-data \
|
||||||
|
;fi
|
||||||
|
|
||||||
WORKDIR /var/www/html
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
# Set permissions for Symfony directories
|
||||||
|
RUN mkdir -p var/cache var/log/cron var/log/php var/sessions && \
|
||||||
|
chown -R www-data:www-data var
|
||||||
|
|
||||||
# Default command
|
# Default command
|
||||||
CMD ["php-fpm"]
|
CMD ["php-fpm"]
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* * * * * php /var/www/html/bin/console app:hints:check >> /var/www/html/var/log/cron/cron.log 2>&1
|
||||||
|
5 3 * * * logrotate -s /var/www/html/var/log/.logrotate.state /etc/logrotate.d/cron-hints /etc/logrotate.d/php-logs
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/var/www/html/var/log/cron/cron.log {
|
||||||
|
size 25M
|
||||||
|
rotate 100
|
||||||
|
maxage 90
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
compress
|
||||||
|
delaycompress
|
||||||
|
dateext
|
||||||
|
dateformat -%Y%m%d-%s
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/var/www/html/var/log/php/*.log {
|
||||||
|
size 25M
|
||||||
|
rotate 100
|
||||||
|
maxage 90
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
compress
|
||||||
|
delaycompress
|
||||||
|
dateext
|
||||||
|
dateformat -%Y%m%d-%s
|
||||||
|
}
|
||||||
@@ -7,3 +7,8 @@ opcache.enable=1
|
|||||||
opcache.enable_cli=1
|
opcache.enable_cli=1
|
||||||
opcache.validate_timestamps=1
|
opcache.validate_timestamps=1
|
||||||
opcache.revalidate_freq=0
|
opcache.revalidate_freq=0
|
||||||
|
|
||||||
|
log_errors=On
|
||||||
|
error_log=/var/www/html/var/log/php/error.log
|
||||||
|
session.gc_maxlifetime=1440
|
||||||
|
session.cookie_lifetime=0
|
||||||
|
|||||||
Executable
+57
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Completely restart ONE project stack (prod or a test/staging copy).
|
||||||
|
# Can be run from any directory. Everything is scoped to the Compose project
|
||||||
|
# name so running this from the test checkout never touches the prod stack.
|
||||||
|
#
|
||||||
|
# ./docker/restart.sh # rebuild-less restart of this checkout's stack
|
||||||
|
# ./docker/restart.sh --prune-all # also run host-wide `docker system/builder prune`
|
||||||
|
# # (old behaviour; skip it when another stack shares the host)
|
||||||
|
|
||||||
|
DOCKER_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
ROOT_DIR=$(cd "$DOCKER_DIR/.." && pwd)
|
||||||
|
|
||||||
|
PRUNE_ALL=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--prune-all) PRUNE_ALL=1 ;;
|
||||||
|
*) echo "Unknown option: $arg" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Read the identifiers from docker/.env (same file Compose uses). STACK_NAME is
|
||||||
|
# the container-name prefix; COMPOSE_PROJECT_NAME is the compose project.
|
||||||
|
read_env() { [ -f "$DOCKER_DIR/.env" ] && grep -E "^$1=" "$DOCKER_DIR/.env" | tail -n1 | cut -d= -f2- | tr -d "\"'" || true; }
|
||||||
|
STACK="$(read_env STACK_NAME)"; STACK="${STACK:-escapepage}"
|
||||||
|
PROJECT="$(read_env COMPOSE_PROJECT_NAME)"; PROJECT="${PROJECT:-$STACK}"
|
||||||
|
echo "Restarting stack: $STACK (compose project: $PROJECT)"
|
||||||
|
|
||||||
|
echo "Stopping and removing containers..."
|
||||||
|
(cd "$DOCKER_DIR" && docker compose -p "$PROJECT" -f compose.yaml -f compose.override.yaml down -v --remove-orphans) || true
|
||||||
|
|
||||||
|
# Belt-and-suspenders: drop anything still lingering for THIS stack only.
|
||||||
|
docker rm -f \
|
||||||
|
"${STACK}-db" "${STACK}-php" "${STACK}-nginx" "${STACK}-mercure" \
|
||||||
|
"${STACK}-mailer" "${STACK}-php-worker" "${STACK}-php-cron" 2>/dev/null || true
|
||||||
|
for net in $(docker network ls -q --filter "name=^${PROJECT}_" 2>/dev/null); do
|
||||||
|
docker network rm "$net" || true
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$PRUNE_ALL" -eq 1 ]; then
|
||||||
|
echo "Host-wide prune (containers, networks, build cache)..."
|
||||||
|
docker system prune -f || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
else
|
||||||
|
echo "Skipping host-wide prune (pass --prune-all to force it)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Setting permissions for var/volumes/db and var directories..."
|
||||||
|
sudo chown -R 1000:1000 "$ROOT_DIR/var/volumes/db" || true
|
||||||
|
sudo chmod -R 777 "$ROOT_DIR/var/volumes/db" || true
|
||||||
|
sudo mkdir -p "$ROOT_DIR/var/cache" "$ROOT_DIR/var/log/cron" "$ROOT_DIR/var/log/php" "$ROOT_DIR/var/sessions"
|
||||||
|
sudo chown -R 1000:1000 "$ROOT_DIR/var" || true
|
||||||
|
sudo chmod -R 777 "$ROOT_DIR/var" || true
|
||||||
|
|
||||||
|
echo "Running setup script..."
|
||||||
|
"$DOCKER_DIR/setup.sh" --no-build
|
||||||
Executable
+84
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Restart the TEST (staging) stack. Scoped entirely to this checkout's compose
|
||||||
|
# project, so it can never touch the production stack. Unlike docker/restart.sh
|
||||||
|
# it:
|
||||||
|
# - keeps the database by default (you loaded prod data into it) — pass
|
||||||
|
# --fresh-db to wipe var/volumes/db and let MySQL re-initialise
|
||||||
|
# - never runs a host-wide `docker system/builder prune`
|
||||||
|
# - only repairs ownership of var/cache, var/log, var/sessions — NEVER
|
||||||
|
# var/volumes/db (that dir belongs to the mysql process; chowning it is
|
||||||
|
# what corrupts the data directory)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./docker/restart.test.sh # down + up (keeps DB and images)
|
||||||
|
# ./docker/restart.test.sh --build # also rebuild images
|
||||||
|
# ./docker/restart.test.sh --fresh-db # also wipe + re-initialise the database
|
||||||
|
|
||||||
|
DOCKER_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
ROOT_DIR=$(cd "$DOCKER_DIR/.." && pwd)
|
||||||
|
|
||||||
|
env_val() { [ -f "$DOCKER_DIR/.env" ] && grep -E "^$1=" "$DOCKER_DIR/.env" | tail -n1 | cut -d= -f2- | tr -d "\"' " || true; }
|
||||||
|
|
||||||
|
if [ ! -f "$DOCKER_DIR/.env" ]; then
|
||||||
|
echo "Error: $DOCKER_DIR/.env not found. Copy docker/.env.test.example to docker/.env first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
PROJECT="$(env_val COMPOSE_PROJECT_NAME)"
|
||||||
|
STACK="$(env_val STACK_NAME)"; STACK="${STACK:-$PROJECT}"
|
||||||
|
case "${PROJECT:-}" in
|
||||||
|
""|escapepage|docker)
|
||||||
|
echo "Error: COMPOSE_PROJECT_NAME in docker/.env is '${PROJECT:-<unset>}'." >&2
|
||||||
|
echo "Refusing to run a test script against the production stack." >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
DOCKER_COMPOSE="docker compose"
|
||||||
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
DOCKER_COMPOSE="docker-compose"
|
||||||
|
else
|
||||||
|
echo "Error: neither 'docker compose' nor 'docker-compose' is available." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
FRESH_DB=0; BUILD=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--fresh-db) FRESH_DB=1 ;;
|
||||||
|
--build) BUILD=1 ;;
|
||||||
|
*) echo "Unknown option: $arg" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
dc() { (cd "$DOCKER_DIR" && $DOCKER_COMPOSE -p "$PROJECT" -f compose.yaml -f compose.override.yaml "$@"); }
|
||||||
|
|
||||||
|
echo "Restarting test stack: $STACK (compose project: $PROJECT)"
|
||||||
|
|
||||||
|
echo "Stopping containers..."
|
||||||
|
dc down --remove-orphans || true
|
||||||
|
# scoped fallback — only this stack
|
||||||
|
docker rm -f \
|
||||||
|
"${STACK}-db" "${STACK}-php" "${STACK}-nginx" "${STACK}-mercure" \
|
||||||
|
"${STACK}-mailer" "${STACK}-php-worker" "${STACK}-php-cron" 2>/dev/null || true
|
||||||
|
for net in $(docker network ls -q --filter "name=^${PROJECT}_" 2>/dev/null); do
|
||||||
|
docker network rm "$net" || true
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$FRESH_DB" -eq 1 ]; then
|
||||||
|
echo "Wiping the test database (var/volumes/db)..."
|
||||||
|
sudo rm -rf "$ROOT_DIR/var/volumes/db"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Repairing var/ ownership (cache/log/sessions only)..."
|
||||||
|
sudo mkdir -p "$ROOT_DIR/var/cache" "$ROOT_DIR/var/log/php" "$ROOT_DIR/var/log/cron" "$ROOT_DIR/var/sessions"
|
||||||
|
sudo chown -R 1000:1000 "$ROOT_DIR/var/cache" "$ROOT_DIR/var/log" "$ROOT_DIR/var/sessions"
|
||||||
|
sudo chmod -R u+rwX,g+rwX "$ROOT_DIR/var/cache" "$ROOT_DIR/var/log" "$ROOT_DIR/var/sessions"
|
||||||
|
|
||||||
|
echo "Bringing the stack back up..."
|
||||||
|
if [ "$BUILD" -eq 1 ]; then
|
||||||
|
"$DOCKER_DIR/setup.test.sh"
|
||||||
|
else
|
||||||
|
"$DOCKER_DIR/setup.test.sh" --no-build
|
||||||
|
fi
|
||||||
Regular → Executable
+19
-7
@@ -17,18 +17,18 @@ set -euo pipefail
|
|||||||
|
|
||||||
ROOT_DIR=$(cd "$(dirname "$0")"/.. && pwd)
|
ROOT_DIR=$(cd "$(dirname "$0")"/.. && pwd)
|
||||||
DOCKER_DIR="$ROOT_DIR/docker"
|
DOCKER_DIR="$ROOT_DIR/docker"
|
||||||
# Determine the docker compose command (V2 'docker compose' or V1 'docker-compose')
|
# Determine the docker-compose command
|
||||||
if docker compose version >/dev/null 2>&1; then
|
if docker compose version >/dev/null 2>&1; then
|
||||||
DOCKER_COMPOSE="docker compose"
|
DOCKER_COMPOSE="docker compose"
|
||||||
elif command -v docker-compose >/dev/null 2>&1; then
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
DOCKER_COMPOSE="docker-compose"
|
DOCKER_COMPOSE="docker-compose"
|
||||||
else
|
else
|
||||||
echo "Error: Neither 'docker compose' nor 'docker-compose' was found. Please install Docker Compose." >&2
|
echo "Error: Neither 'docker-compose' nor 'docker compose' was found. Please install Docker Compose." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Helper to run docker compose from the docker/ directory
|
# Helper to run docker compose from the docker directory
|
||||||
dc() { (cd "$DOCKER_DIR" && $DOCKER_COMPOSE -f compose.yaml "$@"); }
|
dc() { (cd "$DOCKER_DIR" && $DOCKER_COMPOSE -f compose.yaml -f compose.override.yaml "$@"); }
|
||||||
|
|
||||||
REBUILD=1
|
REBUILD=1
|
||||||
RECREATE=0
|
RECREATE=0
|
||||||
@@ -61,7 +61,7 @@ if [ "$RECREATE" -eq 1 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Start stack
|
# Start stack
|
||||||
dc up "${BUILD_ARGS[@]}"
|
dc up -d "${BUILD_ARGS[@]}"
|
||||||
|
|
||||||
# Helper to run commands in php container
|
# Helper to run commands in php container
|
||||||
pexec() { dc exec -T php "$@"; }
|
pexec() { dc exec -T php "$@"; }
|
||||||
@@ -104,9 +104,17 @@ fi
|
|||||||
|
|
||||||
# Prepare DB
|
# Prepare DB
|
||||||
echo "Creating database if it doesn't exist..."
|
echo "Creating database if it doesn't exist..."
|
||||||
pexec php bin/console doctrine:database:create --if-not-exists
|
if ! pexec php bin/console doctrine:database:create --if-not-exists; then
|
||||||
|
echo "Error: Database creation failed. Check Docker logs for details." >&2
|
||||||
|
dc logs database
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Running migrations..."
|
echo "Running migrations..."
|
||||||
pexec php bin/console doctrine:migrations:migrate -n
|
if ! pexec php bin/console doctrine:migrations:migrate -n; then
|
||||||
|
echo "Error: Migrations failed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Import JS deps (Importmap/Asset Mapper)
|
# Import JS deps (Importmap/Asset Mapper)
|
||||||
if [ -f "$ROOT_DIR/importmap.php" ]; then
|
if [ -f "$ROOT_DIR/importmap.php" ]; then
|
||||||
@@ -135,6 +143,10 @@ Common commands:
|
|||||||
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE logs -f nginx)
|
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE logs -f nginx)
|
||||||
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE logs -f php)
|
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE logs -f php)
|
||||||
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE logs -f php-worker)
|
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE logs -f php-worker)
|
||||||
|
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE logs -f php-cron) # crond scheduler activity
|
||||||
|
tail -f "$ROOT_DIR/var/log/cron/cron.log" # hint-check command output
|
||||||
|
tail -f "$ROOT_DIR/var/log/php/error.log" # raw PHP errors
|
||||||
|
tail -f "$ROOT_DIR/var/log/php/prod.log" # Symfony app errors (prod only)
|
||||||
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE exec php bash)
|
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE exec php bash)
|
||||||
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE exec php npm run watch)
|
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE exec php npm run watch)
|
||||||
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE down)
|
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE down)
|
||||||
|
|||||||
Executable
+134
@@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Bootstrap / re-provision a TEST (staging) stack — e.g. test.escapepage.com.
|
||||||
|
# Same job as docker/setup.sh, but:
|
||||||
|
# - refuses to run unless docker/.env marks this as a non-prod stack
|
||||||
|
# - scopes every compose call to COMPOSE_PROJECT_NAME
|
||||||
|
# - runs DB / cache / console steps as www-data and repairs var/ ownership at
|
||||||
|
# the end, so php-fpm (www-data) can actually read the compiled container
|
||||||
|
# (bare `docker exec` runs as root and would leave var/cache root-owned)
|
||||||
|
# - NEVER touches var/volumes/db (MySQL owns that)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./docker/setup.test.sh # full setup (build images)
|
||||||
|
# ./docker/setup.test.sh --no-build # skip image rebuild
|
||||||
|
# ./docker/setup.test.sh --recreate # force-recreate containers
|
||||||
|
# ./docker/setup.test.sh --down # stop and remove this stack's containers
|
||||||
|
|
||||||
|
DOCKER_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
ROOT_DIR=$(cd "$DOCKER_DIR/.." && pwd)
|
||||||
|
|
||||||
|
env_val() { [ -f "$DOCKER_DIR/.env" ] && grep -E "^$1=" "$DOCKER_DIR/.env" | tail -n1 | cut -d= -f2- | tr -d "\"' " || true; }
|
||||||
|
|
||||||
|
# --- safety gate: never let a *.test.sh script act on the production stack ----
|
||||||
|
if [ ! -f "$DOCKER_DIR/.env" ]; then
|
||||||
|
echo "Error: $DOCKER_DIR/.env not found. Copy docker/.env.test.example to docker/.env and fill it in." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
PROJECT="$(env_val COMPOSE_PROJECT_NAME)"
|
||||||
|
STACK="$(env_val STACK_NAME)"; STACK="${STACK:-$PROJECT}"
|
||||||
|
case "${PROJECT:-}" in
|
||||||
|
""|escapepage|docker)
|
||||||
|
echo "Error: COMPOSE_PROJECT_NAME in docker/.env is '${PROJECT:-<unset>}'." >&2
|
||||||
|
echo "Refusing to run a test script against the production stack." >&2
|
||||||
|
echo "Set COMPOSE_PROJECT_NAME and STACK_NAME to e.g. 'escapepage-test' in docker/.env." >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- compose command -------------------------------------------------------
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
DOCKER_COMPOSE="docker compose"
|
||||||
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
DOCKER_COMPOSE="docker-compose"
|
||||||
|
else
|
||||||
|
echo "Error: neither 'docker compose' nor 'docker-compose' is available." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
command -v docker >/dev/null 2>&1 || { echo "Error: docker is required." >&2; exit 1; }
|
||||||
|
|
||||||
|
dc() { (cd "$DOCKER_DIR" && $DOCKER_COMPOSE -p "$PROJECT" -f compose.yaml -f compose.override.yaml "$@"); }
|
||||||
|
pexec() { dc exec -T php "$@"; } # as root (composer / npm)
|
||||||
|
pexecwww() { dc exec -T -u www-data php "$@"; } # as www-data (console / DB / cache)
|
||||||
|
|
||||||
|
REBUILD=1; RECREATE=0; DOWN_ONLY=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--no-build) REBUILD=0 ;;
|
||||||
|
--recreate) RECREATE=1 ;;
|
||||||
|
--down) DOWN_ONLY=1 ;;
|
||||||
|
*) echo "Unknown option: $arg" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Test stack: $STACK (compose project: $PROJECT)"
|
||||||
|
|
||||||
|
if [ "$DOWN_ONLY" -eq 1 ]; then
|
||||||
|
dc down --remove-orphans
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
BUILD_ARGS=()
|
||||||
|
[ "$REBUILD" -eq 1 ] && BUILD_ARGS+=("--build")
|
||||||
|
[ "$RECREATE" -eq 1 ] && BUILD_ARGS+=("--force-recreate")
|
||||||
|
|
||||||
|
dc up -d "${BUILD_ARGS[@]}"
|
||||||
|
|
||||||
|
# --- wait for the database ------------------------------------------------
|
||||||
|
printf "Waiting for database to be healthy..."
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
DB_ID=$(dc ps -q database 2>/dev/null || true)
|
||||||
|
if [ -n "$DB_ID" ] && [ "$(docker inspect -f '{{.State.Health.Status}}' "$DB_ID" 2>/dev/null || true)" = "healthy" ]; then
|
||||||
|
echo " OK"; break
|
||||||
|
fi
|
||||||
|
printf "."; sleep 2
|
||||||
|
[ "$i" -eq 60 ] && echo -e "\nWarning: database not healthy yet, continuing anyway."
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- dependencies (root: writes vendor/ and node_modules/) --------------
|
||||||
|
pexec composer install --no-interaction
|
||||||
|
|
||||||
|
# --- APP_SECRET: generate into .env.local if it's set nowhere -----------
|
||||||
|
if ! grep -qsE '^APP_SECRET=.+' "$ROOT_DIR/.env" "$ROOT_DIR/.env.local"; then
|
||||||
|
echo "Generating APP_SECRET in .env.local..."
|
||||||
|
printf 'APP_SECRET=%s\n' "$(openssl rand -hex 16)" >> "$ROOT_DIR/.env.local"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- database (www-data: keeps var/ writable by php-fpm) ---------------
|
||||||
|
echo "Creating database if it doesn't exist..."
|
||||||
|
pexecwww php bin/console doctrine:database:create --if-not-exists || {
|
||||||
|
echo "Error: database creation failed." >&2; dc logs database | tail -n 40; exit 1;
|
||||||
|
}
|
||||||
|
echo "Running migrations..."
|
||||||
|
pexecwww php bin/console doctrine:migrations:migrate -n || { echo "Error: migrations failed." >&2; exit 1; }
|
||||||
|
|
||||||
|
[ -f "$ROOT_DIR/importmap.php" ] && pexec php bin/console importmap:install || true
|
||||||
|
|
||||||
|
if [ -f "$ROOT_DIR/package.json" ]; then
|
||||||
|
echo "Installing npm dependencies..."; pexec npm ci || pexec npm install
|
||||||
|
echo "Building assets..."; pexec npm run build
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- repair var/ ownership for php-fpm (www-data), never var/volumes ----
|
||||||
|
echo "Fixing var/ ownership for php-fpm..."
|
||||||
|
pexec sh -lc 'mkdir -p var/cache var/log/php var/log/cron var/sessions && chown -R www-data:www-data var/cache var/log var/sessions'
|
||||||
|
|
||||||
|
pexecwww php bin/console cache:clear
|
||||||
|
|
||||||
|
NGINX_HTTPS="$(env_val NGINX_HTTPS_PORT)"
|
||||||
|
MAILPIT_UI="$(env_val MAILPIT_UI_PORT)"
|
||||||
|
|
||||||
|
cat <<EOT
|
||||||
|
|
||||||
|
Test stack '$PROJECT' is up.
|
||||||
|
|
||||||
|
NPM upstream: ${STACK}-nginx (scheme https, port 443, "Verify SSL" off)
|
||||||
|
Local check: curl -k https://${NGINX_HTTPS:-127.0.0.1:18443}/
|
||||||
|
Mailpit UI: http://${MAILPIT_UI:-127.0.0.1:18026}
|
||||||
|
|
||||||
|
Logs: docker logs -f ${STACK}-php
|
||||||
|
Shell: docker exec -it -u www-data ${STACK}-php sh
|
||||||
|
Restart: ./docker/restart.test.sh (add --fresh-db to wipe + re-init the DB)
|
||||||
|
|
||||||
|
Re-run this script any time. Use --no-build to skip the image rebuild.
|
||||||
|
EOT
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260117143000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Make player.screen nullable';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE player CHANGE screen screen INT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE player CHANGE screen screen INT NOT NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260627140000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Seed initial game: AI Virus Deflection';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql("INSERT INTO game (name, number_of_players, status) VALUES ('AI Virus Deflection', 3, 'inDevelopment')");
|
||||||
|
$this->addSql("INSERT INTO game_setting (game_id, name, value) VALUES (LAST_INSERT_ID(), 'totalTime', '1800')");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql("DELETE gs FROM game_setting gs INNER JOIN game g ON gs.game_id = g.id WHERE g.name = 'AI Virus Deflection'");
|
||||||
|
$this->addSql("DELETE FROM game WHERE name = 'AI Virus Deflection'");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260704160000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add marketing_opt_in column to user table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE `user` ADD marketing_opt_in TINYINT(1) NOT NULL DEFAULT 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE `user` DROP marketing_opt_in');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260711210000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add deleted_at and last_login_at to user table; cascade-delete email_log and reset_password_request rows';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE `user` ADD deleted_at DATETIME DEFAULT NULL, ADD last_login_at DATETIME DEFAULT NULL');
|
||||||
|
|
||||||
|
$this->addSql('ALTER TABLE email_log DROP FOREIGN KEY FK_6FB4883A76ED395');
|
||||||
|
$this->addSql('ALTER TABLE email_log ADD CONSTRAINT FK_6FB4883A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id) ON DELETE CASCADE');
|
||||||
|
|
||||||
|
$this->addSql('ALTER TABLE reset_password_request DROP FOREIGN KEY FK_7CE748AA76ED395');
|
||||||
|
$this->addSql('ALTER TABLE reset_password_request ADD CONSTRAINT FK_7CE748AA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id) ON DELETE CASCADE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE email_log DROP FOREIGN KEY FK_6FB4883A76ED395');
|
||||||
|
$this->addSql('ALTER TABLE email_log ADD CONSTRAINT FK_6FB4883A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
|
||||||
|
|
||||||
|
$this->addSql('ALTER TABLE reset_password_request DROP FOREIGN KEY FK_7CE748AA76ED395');
|
||||||
|
$this->addSql('ALTER TABLE reset_password_request ADD CONSTRAINT FK_7CE748AA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
|
||||||
|
|
||||||
|
$this->addSql('ALTER TABLE `user` DROP deleted_at, DROP last_login_at');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260810120000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add lobby_message table for pre-game lobby chat';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE lobby_message (id INT AUTO_INCREMENT NOT NULL, session_id INT NOT NULL, player_id INT NOT NULL, content VARCHAR(500) NOT NULL, created_at DATETIME NOT NULL, INDEX IDX_LOBBY_MESSAGE_SESSION (session_id), INDEX IDX_LOBBY_MESSAGE_PLAYER (player_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('ALTER TABLE lobby_message ADD CONSTRAINT FK_LOBBY_MESSAGE_SESSION FOREIGN KEY (session_id) REFERENCES session (id)');
|
||||||
|
$this->addSql('ALTER TABLE lobby_message ADD CONSTRAINT FK_LOBBY_MESSAGE_PLAYER FOREIGN KEY (player_id) REFERENCES player (id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE lobby_message DROP FOREIGN KEY FK_LOBBY_MESSAGE_SESSION');
|
||||||
|
$this->addSql('ALTER TABLE lobby_message DROP FOREIGN KEY FK_LOBBY_MESSAGE_PLAYER');
|
||||||
|
$this->addSql('DROP TABLE lobby_message');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260810130000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add finished_at column to session table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE session ADD finished_at DATETIME DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE session DROP finished_at');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260810140000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create hint table and seed it with the previously-hardcoded mainframe hints for every existing game';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE hint (id INT AUTO_INCREMENT NOT NULL, game_id INT NOT NULL, hint_condition VARCHAR(30) NOT NULL, threshold_seconds INT NOT NULL, message LONGTEXT NOT NULL, sort_order INT NOT NULL, INDEX IDX_HINT_GAME (game_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('ALTER TABLE hint ADD CONSTRAINT FK_HINT_GAME FOREIGN KEY (game_id) REFERENCES `game` (id) ON DELETE CASCADE');
|
||||||
|
|
||||||
|
// Seed every existing game with the hints that used to be hardcoded in
|
||||||
|
// SendMainframeHintsCommand, so behavior doesn't regress the moment the
|
||||||
|
// command switches to reading from this table.
|
||||||
|
$seed = [
|
||||||
|
['help_used', 120, 'Have you already checked which functions are available to you through the help command?', 10],
|
||||||
|
['broadcast_done', 300, 'You should establish communications with your fellow agents.', 20],
|
||||||
|
['contacted_all_privately', 420, 'The AI virus can see all communication if you are using open communication channels. Make sure you send private messages to all other agents as well, so I know you can.', 30],
|
||||||
|
['verified', 600, 'You need the help of your fellow agents to verify you. If both other agents have helped you in your verification, i can get you more rights. The help command might give you some more information.', 40],
|
||||||
|
['verified', 780, 'You are still not verified! Please get your verification codes from your colleagues so you can verify.', 50],
|
||||||
|
['left_home_directory', 900, 'You can change the folder you are working in. Check the help command for more information', 60],
|
||||||
|
['all_decoded', 1020, 'You should go to the rapports directory to check out the rapports.', 70],
|
||||||
|
['all_decoded', 1140, "Not all messages can be decoded by every agent. Every agent has their own personal decoding method. If you can't decode a message, maybe a colleague can.", 80],
|
||||||
|
['none', 1380, 'Have you checked out the help command to see what you can do?', 90],
|
||||||
|
['none', 1560, 'HURRY! The AI virus is almost done decoding the last files before it will send everything out!', 100],
|
||||||
|
['none', 1680, 'Please remove the files soon! The AI virus can not win! It will be a disaster if it does!', 110],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($seed as [$condition, $threshold, $message, $sortOrder]) {
|
||||||
|
$this->addSql(
|
||||||
|
'INSERT INTO hint (game_id, hint_condition, threshold_seconds, message, sort_order) '
|
||||||
|
. 'SELECT id, ?, ?, ?, ? FROM `game`',
|
||||||
|
[$condition, $threshold, $message, $sortOrder]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE hint DROP FOREIGN KEY FK_HINT_GAME');
|
||||||
|
$this->addSql('DROP TABLE hint');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260829120000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create contact_message table for the public contact form';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE contact_message (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, message LONGTEXT NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE contact_message');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260829130000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add read_at to contact_message so admin can track which messages have been read';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE contact_message ADD read_at DATETIME DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE contact_message DROP read_at');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260829140000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add deleted_at to contact_message for soft-deleting messages from the admin list';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE contact_message ADD deleted_at DATETIME DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE contact_message DROP deleted_at');
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1774
-4017
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user