69 Commits

Author SHA1 Message Date
Frank
0a9c0f0052 Fixed network v4 2026-03-11 07:59:23 +01:00
Frank
57af01ea6f Fixed network v3 2026-03-11 07:57:49 +01:00
Frank
4b0f8fb5c6 Fixed network v2 2026-03-11 07:55:47 +01:00
Frank
a68eeb3ac6 Fixed network 2026-03-11 07:52:11 +01:00
Frank
cc6c6a6456 Fixed ip addresses 2026-03-10 22:13:22 +01:00
Frank
184a5ff0de Fix database container creation 2026-03-10 22:04:23 +01:00
Frank
170ae2ce43 Unfixed ips 2026-03-10 22:00:08 +01:00
Frank
16625c557c Fixed ips 2026-03-10 21:55:21 +01:00
Frank
a15edfabed network defined 2026-03-10 21:52:05 +01:00
66a682f69f Prod development 2026-02-21 14:28:48 +00:00
Frank
576d86f602 Mercure ssl 2026-01-17 22:56:34 +01:00
Frank
891c1b4ecb Mercure cors error 3 2026-01-17 19:36:01 +01:00
Frank
f5bac0fe2d Mercure cors error 2 2026-01-17 19:27:01 +01:00
Frank
0276199488 Mercure cors error 2026-01-17 19:03:25 +01:00
Frank
83381ef57f Mercure links 2026-01-17 15:49:58 +01:00
Frank
c6153c62f1 nullable screen 2026-01-17 15:35:26 +01:00
Frank
4f7471ab0f Remote allowance 2026-01-17 15:12:08 +01:00
Frank
9237d9ad49 captcha keys 2026-01-17 14:51:14 +01:00
Frank
a3573d5a09 Update passwords MySql 2026-01-17 14:29:04 +01:00
Frank
5e87ae90d8 executable 2026-01-17 14:24:54 +01:00
Frank
05a514bad6 restart via 1 script. 2026-01-17 14:22:08 +01:00
Frank
3a34266461 Verification mails solving try 1 2026-01-17 14:12:57 +01:00
Frank
7fe8f9322a Verification mails solving try 1 2026-01-17 13:47:56 +01:00
Frank
27827bd2a9 Added domain to verification mail 2026-01-16 21:01:01 +01:00
Frank
de1c6f4ed2 Send variables to containers. 2026-01-14 14:00:11 +01:00
Frank
a6df6cbf0c Request resend of verification mail 2026-01-14 13:09:32 +01:00
Frank
a90489da28 captcha 2026-01-13 21:54:26 +01:00
Frank
498ba1bfca Verifying mail addresses 2026-01-13 17:43:17 +01:00
Frank
f96e51420f Mailer From 2026-01-11 23:10:30 +01:00
Frank
f3bf472bc6 Try to fix 3 2026-01-11 16:00:43 +01:00
Frank
34d89129ae Try to fix 2 2026-01-11 15:56:52 +01:00
Frank
0c0c71c7b4 Try to fix 1 2026-01-11 15:46:46 +01:00
Frank
d70ef9282e Revert compose files 2026-01-11 15:41:40 +01:00
Frank
de67d95d4f Niet weggooien van images 4 2026-01-11 15:38:33 +01:00
Frank
bcb42a27b8 Niet weggooien van images 3 2026-01-11 15:37:13 +01:00
Frank
bc4f7a8c79 Niet weggooien van images 2 2026-01-11 15:35:10 +01:00
Frank
3b98e8650b onbekende flag 5 2026-01-11 15:30:21 +01:00
Frank
09f9abcfb8 onbekende flag 4 2026-01-11 15:28:40 +01:00
Frank
3b3cb69aa7 onbekende flag 3 2026-01-11 15:26:34 +01:00
Frank
badca1af53 onbekende flag 2 2026-01-11 15:25:03 +01:00
Frank
b649d48250 onbekende flag 2026-01-11 15:21:19 +01:00
Frank
0e3a3992fa container en cache clear erin en image clear eruit 2026-01-10 17:21:59 +01:00
Frank
cfac6d10ec csrf error solve. try 6 2026-01-10 16:25:07 +01:00
Frank
a10ad7de58 csrf error solve. try 5 2026-01-10 14:28:03 +01:00
Frank
f810ad07b7 csrf error solve. try 4 2026-01-10 14:19:57 +01:00
Frank
41f3547f6f csrf error solve. try 3 2026-01-10 14:06:29 +01:00
Frank
09b7e78fdd csrf error solve. try 2 2026-01-10 13:37:14 +01:00
Frank
47091cd4e3 csrf error solve. try 1 2026-01-10 00:39:33 +01:00
Frank
ac4c5ef261 Validation fails 2026-01-10 00:25:57 +01:00
Frank
490f730c97 pass on token 2026-01-10 00:17:36 +01:00
Frank
73d6ea478c Restart containers 2026-01-09 23:40:25 +01:00
Frank
9da6a60dbe Add error log 2026-01-09 23:36:50 +01:00
Frank
7ca0bec145 Mercure en hostfile 2026-01-09 18:38:57 +01:00
Frank
ea54c87426 database volume 2026-01-09 16:53:47 +01:00
Frank
12e87edc4d Fixed ips 2026-01-09 16:42:15 +01:00
Frank
85416f5a07 rights to user 2026-01-09 16:10:44 +01:00
Frank
2f81a60ff7 Meer updates. Next try 7 2026-01-09 15:47:44 +01:00
Frank
0e27217ab4 Meer updates. Next try 6 2026-01-09 15:42:43 +01:00
Frank
b3531b5d7c Meer updates. Next try 5 2026-01-09 15:33:52 +01:00
Frank
239b1e136a Meer updates. Next try 4 2026-01-09 15:25:41 +01:00
Frank
db04cafcb1 Meer updates. Next try 3 2026-01-09 15:18:25 +01:00
Frank
5ce7e15565 Meer updates. Next try 2 2026-01-09 15:10:44 +01:00
Frank
df12c4bd11 Meer updates. Next try 2026-01-09 15:01:12 +01:00
Frank
769d8b4e74 Meer updates. Hopelijk beter nu. 2026-01-09 14:51:31 +01:00
Frank
14871336a3 Updated dockerfile om migrations uit te kunnen voeren 2026-01-09 14:41:59 +01:00
Frank
3604c63940 Running containers 2026-01-09 14:30:05 +01:00
7257c51bdf Merge pull request 'Settings from env files' (#13) from env-settings into main
Reviewed-on: #13
2026-01-09 13:21:02 +00:00
Frank
641573842c Settings from env files 2026-01-09 13:08:09 +01:00
abc3712d97 Merge pull request 'timer-af-laten-lopen' (#12) from timer-af-laten-lopen into main
Reviewed-on: #12
2026-01-09 11:23:39 +00:00
53 changed files with 821 additions and 171 deletions

25
.dockerignore Normal file
View File

@@ -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

48
.env
View File

@@ -15,17 +15,30 @@
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=
APP_ENV=prod
APP_SECRET=695679907f9c3818e6924d547f872651
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"
DATABASE_URL="mysql://escapepage:b.0nqrxJ%%2FD%%2ALuf9N@localhost:3306/escapepage?serverVersion=8.0.32&charset=utf8mb4"
DB_DRIVER=pdo_mysql
DB_SERVER_VERSION=8.0.32
DB_CHARSET=utf8mb4
DB_USER=escapepage
DB_PASSWORD=Zr1aOYU5NpCbS3dhpxa64cZp
DB_HOST=database
DB_PORT=3306
DB_NAME=escapepage
MYSQL_ROOT_PASSWORD=root
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 ###
@@ -37,14 +50,13 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###> symfony/mailer ###
# Development: use Mailpit (docker compose override provides service `mailer` on port 1025)
MAILER_DSN=smtp://mailer:1025
MAILER_DSN=sendgrid://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@default
MAILER_FROM=mailer@escapepage.nl
# Production/Stage (uncomment and set SENDGRID_API_KEY in real env or secrets):
# MAILER_DSN=sendgrid+api://%env(SENDGRID_API_KEY)%
# Alternatively, via SMTP (no extra package needed):
# MAILER_DSN="smtp://apikey:%env(SENDGRID_API_KEY)%@smtp.sendgrid.net:587?encryption=tls"
# Optional default sender (used by test command if --from not passed):
# MAILER_FROM=no-reply@your-domain.tld
# SENDGRID_API_KEY=your_real_key_goes_here # Do NOT commit this; set in .env.local or deployment env
###< symfony/mailer ###
###> symfony/sendgrid-mailer ###
@@ -53,11 +65,27 @@ MAILER_DSN=smtp://mailer:1025
###> mercure ###
# Internal hub URL used by the PHP app (reachable from the php container)
MERCURE_URL=http://mercure/.well-known/mercure
MERCURE_URL=https://mercure/.well-known/mercure
# Public hub URL used by browsers
MERCURE_PUBLIC_URL=http://localhost:8090/.well-known/mercure
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!
# Base URL for Mercure topics. Use .dev in development; override to .com in prod via .env.prod or real env.
MERCURE_TOPIC_BASE=https://escapepage.dev
# Pre-generated JWT tokens for convenience
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=6LdIvk0sAAAAAC2jMbBXtjDQC24mmNbwHWBulxFu
RECAPTCHA3_SECRET=6LdIvk0sAAAAAE9TCGAQoczQFwR6l2dxkkwcPKsk
###< karser/karser-recaptcha3-bundle ###

17
.env.dev Normal file
View File

@@ -0,0 +1,17 @@
###> symfony/framework-bundle ###
APP_SECRET=620e9ce5f88a714b636179eb39d5be4f
###< symfony/framework-bundle ###
###> mercure ###
MERCURE_CORS_ALLOWED_ORIGINS=http://localhost:8080
MERCURE_TOPIC_BASE=https://escapepage.dev
MERCURE_PUBLISHER_JWT_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.E5b7ma4k-kA7lVGOQtICh7r2sspwX4G1iOhwtbxHQck
MERCURE_SUBSCRIBER_JWT_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIqIl19fQ.mwSAjvbm6vOnjMoRSHMdcqapNCwyGZs1s57uLK4T3UM
###< mercure ###
DB_HOST=database
DB_PORT=3306
DB_NAME=escapepage
DB_USER=escapepage
DB_PASSWORD="b.0nqrxJ/D*Luf9N"

37
.env.prod Normal file
View File

@@ -0,0 +1,37 @@
APP_ENV=prod
APP_SECRET=a8f89e179e8c338423697669d6728c2c
### Compiled or real environment variables should be used in production.
### Configure MAILER_DSN to use SendGrid API transport.
### Prefer storing SENDGRID_API_KEY using Symfony Secrets or real env vars.
###> symfony/mailer ###
MAILER_DSN=sendgrid://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@default
MAILER_FROM=mailer@escapepage.nl
###< symfony/mailer ###
###> symfony/framework-bundle ###
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
###> mercure ###
# Use the production URL for CORS in production
MERCURE_JWT_SECRET=55UtgFXsZu09TSTdeIA7ljK4HUo9DLkRzEB7MD5tqOLjRfAb
MERCURE_PUBLISHER_JWT_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.qMVdzh7buYK78e-gwCQx7v6qCxk1Js83SAEKK-GZSrI
MERCURE_SUBSCRIBER_JWT_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIqIl19fQ.OCnRPXfCoke27ntAxby2R5jkgpTZdw83DPq1yhvkLbw
MERCURE_CORS_ALLOWED_ORIGINS="https://escapepage.com"
MERCURE_TOPIC_BASE=https://escapepage.com
###< mercure ###
DB_HOST=database
DB_PORT=3306
DB_NAME=escapepage
DB_USER=escapepage
DB_PASSWORD=Zr1aOYU5NpCbS3dhpxa64cZp
###> docker ###
USER_ID=1000
GROUP_ID=1000
###< docker ###

18
.env.test Normal file
View File

@@ -0,0 +1,18 @@
APP_ENV=test
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
###> mercure ###
MERCURE_CORS_ALLOWED_ORIGINS=http://localhost:8080
MERCURE_TOPIC_BASE=http://test
MERCURE_PUBLISHER_JWT_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.E5b7ma4k-kA7lVGOQtICh7r2sspwX4G1iOhwtbxHQck
MERCURE_SUBSCRIBER_JWT_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIqIl19fQ.mwSAjvbm6vOnjMoRSHMdcqapNCwyGZs1s57uLK4T3UM
###< mercure ###
DB_HOST=database
DB_PORT=3306
DB_NAME=escapepage_test
DB_USER=escapepage
DB_PASSWORD="b.0nqrxJ/D*Luf9N"

6
.gitignore vendored
View File

@@ -3,12 +3,12 @@
/.env.local
/.env.local.php
/.env.*.local
/.env.dev
/.env.prod
/.env.test
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
!/var/volumes/
/var/volumes/*
!/var/volumes/.gitignore
/vendor/
###< symfony/framework-bundle ###

1
.idea/escapepage.iml generated
View File

@@ -140,6 +140,7 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/webpack-encore-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/reset-password-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/verify-email-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/karser/karser-recaptcha3-bundle" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

1
.idea/php.xml generated
View File

@@ -153,6 +153,7 @@
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
<path value="$PROJECT_DIR$/vendor/symfonycasts/verify-email-bundle" />
<path value="$PROJECT_DIR$/vendor/symfonycasts/reset-password-bundle" />
<path value="$PROJECT_DIR$/vendor/karser/karser-recaptcha3-bundle" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />

View File

@@ -16,7 +16,7 @@ This repository contains a Symfony 7.3 (PHP >= 8.5.1) application for a collabor
6. Run tests: `vendor/bin/phpunit`
- With Docker:
1. From `docker/`: `docker compose up -d`
1. `cd docker && docker compose up -d`
2. Install vendors inside the PHP container:
- `docker compose exec php bash`
- `composer install`
@@ -28,7 +28,7 @@ This repository contains a Symfony 7.3 (PHP >= 8.5.1) application for a collabor
## Email (Mailpit in dev, SendGrid for prod)
- Dev: a `mailer` service (Mailpit) runs in Docker.
- SMTP DSN in `.env`: `MAILER_DSN=smtp://mailer:1025`
- Mailpit UI: http://localhost:8025
- Mailpit UI: http://localhost:8025 (or mapped port 8025)
- Send a test mail: `php bin/console app:mail:test you@example.com`
- Staging/Prod: use SendGrid.
- Require package (already in composer): `symfony/sendgrid-mailer`.
@@ -81,9 +81,9 @@ See doc/CONTRIBUTING.md for code style and more details.
We use a Mercure hub (Docker service) to push server updates to browsers via ServerSent Events (SSE).
Quick start (dev):
1. Start Docker stack 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.
2. Install PHP deps inside the PHP container if you haven't yet:
@@ -91,12 +91,6 @@ Quick start (dev):
docker compose exec php bash
composer install
```
3. Open the Game Hub page in your browser: http://localhost:8080/game
- The page subscribes to a demo topic and logs messages in the console.
4. Publish a test update (in the PHP container):
```
php bin/console app:mercure:publish
```
You should see a console log like `[Mercure] Update received: { ... }` on the Game Hub page.
Configuration:

View File

@@ -1,7 +0,0 @@
services:
###> symfony/mercure-bundle ###
mercure:
ports:
- "80"
###< symfony/mercure-bundle ###

View File

@@ -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: '!ChangeThisMercureJWTSignedBySymfonySecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureJWTSignedBySymfonySecretKey!'
# Set the URL of your Symfony project (without trailing slash!) as value of the cors_origins directive
MERCURE_EXTRA_DIRECTIVES: |
cors_origins http://localhost:8080
# 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 ###

View File

@@ -11,6 +11,7 @@
"doctrine/doctrine-bundle": "^2.16",
"doctrine/doctrine-migrations-bundle": "^3.4",
"doctrine/orm": "^3.5",
"karser/karser-recaptcha3-bundle": "^0.3.0",
"phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.3",
"symfony/asset": "7.3.*",

84
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "8e2419832c0841e325a5b748bde61a48",
"content-hash": "22d46e70bd8246939c294d28ecfff13d",
"packages": [
{
"name": "composer/semver",
@@ -1271,6 +1271,88 @@
],
"time": "2025-03-06T22:45:56+00:00"
},
{
"name": "karser/karser-recaptcha3-bundle",
"version": "v0.3.0",
"source": {
"type": "git",
"url": "https://github.com/karser/KarserRecaptcha3Bundle.git",
"reference": "3d194dab4c31115bebc073c866ff55afaaa9e276"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/karser/KarserRecaptcha3Bundle/zipball/3d194dab4c31115bebc073c866ff55afaaa9e276",
"reference": "3d194dab4c31115bebc073c866ff55afaaa9e276",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=8.1",
"symfony/expression-language": "^6.4|^7.0|^8.0",
"symfony/form": "^6.4|^7.0|^8.0",
"symfony/framework-bundle": "^6.4|^7.0|^8.0",
"symfony/twig-bundle": "^6.4|^7.0|^8.0",
"symfony/validator": "^6.4|^7.0|^8.0",
"symfony/yaml": "^6.4|^7.0|^8.0",
"twig/twig": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^9|^10|^11",
"symfony/http-client": "^6.4|^7.0|^8.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "0.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Karser\\Recaptcha3Bundle\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dmitrii Poddubnyi",
"homepage": "https://github.com/karser"
}
],
"description": "Google ReCAPTCHA v3 for Symfony",
"homepage": "http://github.com/karser/KarserRecaptcha3Bundle",
"keywords": [
"Forms",
"Google ReCaptcha",
"GoogleReCaptcha",
"Symfony Google ReCaptcha",
"anti-bot",
"anti-bots",
"anti-spam",
"captcha",
"contact",
"google",
"google recaptcha v3",
"no-captcha",
"recaptcha",
"recaptcha v3",
"recaptcha v3 symfony",
"security",
"spam",
"symfony",
"symfony google recaptcha v3",
"symfony recaptcha",
"symfony recaptcha v3",
"validation"
],
"support": {
"issues": "https://github.com/karser/KarserRecaptcha3Bundle/issues",
"source": "https://github.com/karser/KarserRecaptcha3Bundle/tree/v0.3.0"
},
"time": "2025-12-08T11:25:16+00:00"
},
{
"name": "lcobucci/jwt",
"version": "5.6.0",

View File

@@ -17,4 +17,5 @@ return [
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true],
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
Karser\Recaptcha3Bundle\KarserRecaptcha3Bundle::class => ['all' => true],
];

View File

@@ -1,11 +1,10 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
form:
csrf_protection:
token_id: submit
csrf_protection:
stateless_token_ids:
- submit
- authenticate
- logout
# form:
# csrf_protection:
# token_id: submit
# csrf_protection:
# stateless_token_ids:
# - submit
# - authenticate
# - logout

View File

@@ -1,6 +1,14 @@
doctrine:
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,
# either here or in the DATABASE_URL env var (see .env file)

View File

@@ -8,7 +8,21 @@ framework:
fallbacks: ['en', 'nl']
# 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%'
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
#fragments: true

View File

@@ -0,0 +1,5 @@
karser_recaptcha3:
site_key: '%env(RECAPTCHA3_KEY)%'
secret_key: '%env(RECAPTCHA3_SECRET)%'
score_threshold: 0.5
enabled: true

View File

@@ -4,5 +4,4 @@ mercure:
url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%'
jwt:
secret: '%env(MERCURE_JWT_SECRET)%'
publish: ['*']
value: '%env(MERCURE_PUBLISHER_JWT_TOKEN)%'

View File

@@ -2,7 +2,7 @@ framework:
router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
#default_uri: http://localhost
default_uri: '%env(SITE_BASE_URL)%'
when@prod:
framework:

View File

@@ -30,6 +30,7 @@ security:
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/, roles: PUBLIC_ACCESS, requires_channel: https }
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }

View File

@@ -0,0 +1,2 @@
karser_recaptcha3:
enabled: false

View File

@@ -4,6 +4,7 @@
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
mailer_from: '%env(MAILER_FROM)%'
services:
# default configuration for services in *this* file

View File

@@ -4,7 +4,7 @@ Use this index to quickly locate files and directories during development and in
## Top-Level
- docker/compose.yaml / docker/compose.override.yaml — Docker services.
- docker/ — Docker build contexts and configs (php Dockerfile, nginx vhost, compose files).
- docker/ — Docker build contexts and configs (php Dockerfile, nginx vhost).
- composer.json / composer.lock — Dependencies and scripts.
- importmap.php — Importmap configuration for JS dependencies.
- phpunit.dist.xml — PHPUnit configuration.

View File

@@ -9,7 +9,7 @@ This app can run fully in Docker using docker compose with PHP-FPM, Nginx and My
- mailer (dev only via compose.override.yaml): Mailpit (SMTP/UI)
## Prerequisites
- Docker and Docker Compose (v2)
- Docker and Docker Compose (docker compose)
## Usage
@@ -21,36 +21,42 @@ App will be served at http://localhost:8080
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
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
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
docker compose -f docker/compose.yaml -f docker/compose.override.yaml exec php php bin/console doctrine:migrations:migrate -n
cd docker
docker compose exec php php bin/console doctrine:database:create --if-not-exists
docker compose exec php php bin/console doctrine:migrations:migrate -n
```
### 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
```
docker compose -f docker/compose.yaml -f docker/compose.override.yaml logs -f nginx
docker compose -f docker/compose.yaml -f docker/compose.override.yaml logs -f php
cd docker
docker compose logs -f nginx
docker compose logs -f php
```
### 6) Stop
```
docker compose -f docker/compose.yaml -f docker/compose.override.yaml down
cd docker
docker compose down
```
## Notes

View File

@@ -18,7 +18,7 @@ MAILER_DSN=smtp://mailer:1025
```
- Usage:
1. Start stack: `docker compose up -d`
1. Start stack: `docker-compose up -d`
2. Send an email from the app.
3. Open http://localhost:8025 to view captured emails.

56
docker/.env Normal file
View File

@@ -0,0 +1,56 @@
# This file is a template for Docker environment variables.
# It was created by merging .env and .env.prod, with .env.prod taking precedence.
###> symfony/framework-bundle ###
APP_ENV=prod
APP_SECRET=a8f89e179e8c338423697669d6728c2c
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 ###
DB_DRIVER=pdo_mysql
DB_SERVER_VERSION=8.0.32
DB_CHARSET=utf8mb4
DB_USER=escapepage
DB_PASSWORD=Zr1aOYU5NpCbS3dhpxa64cZp
DB_HOST=database
DB_PORT=3306
DB_NAME=escapepage
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 ###
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###< symfony/messenger ###
###> symfony/mailer ###
MAILER_DSN=sendgrid://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@default
MAILER_FROM=mailer@escapepage.nl
###< symfony/mailer ###
###> symfony/sendgrid-mailer ###
# MAILER_DSN=sendgrid://KEY@default
###< symfony/sendgrid-mailer ###
###> mercure ###
MERCURE_URL=https://mercure/.well-known/mercure
MERCURE_PUBLIC_URL=https://escapepage.com:8090/.well-known/mercure
MERCURE_JWT_SECRET=55UtgFXsZu09TSTdeIA7ljK4HUo9DLkRzEB7MD5tqOLjRfAb
MERCURE_PUBLISHER_JWT_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.qMVdzh7buYK78e-gwCQx7v6qCxk1Js83SAEKK-GZSrI
MERCURE_SUBSCRIBER_JWT_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIqIl19fQ.OCnRPXfCoke27ntAxby2R5jkgpTZdw83DPq1yhvkLbw
MERCURE_CORS_ALLOWED_ORIGINS=https://escapepage.com
MERCURE_TOPIC_BASE=https://escapepage.com
###< mercure ###
###> docker ###
USER_ID=1000
GROUP_ID=1000
###< docker ###
###> karser/karser-recaptcha3-bundle ###
RECAPTCHA3_KEY=my_site_key
RECAPTCHA3_SECRET=my_secret
###< karser/karser-recaptcha3-bundle ###

29
docker/.env.dist Normal file
View File

@@ -0,0 +1,29 @@
# User and Group IDs
USER_ID=1000
GROUP_ID=1000
# Application
APP_ENV=prod
SITE_BASE_URL=https://escapepage.com
# Mailer
MAILER_DSN=sendgrid://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@default
MAILER_FROM=mailer@escapepage.nl
# Database
DATABASE_URL=mysql://escapepage:Zr1aOYU5NpCbS3dhpxa64cZp@database:3306/escapepage?serverVersion=8.0.32&charset=utf8mb4
DB_NAME=escapepage
DB_USER=escapepage
DB_PASSWORD=Zr1aOYU5NpCbS3dhpxa64cZp
MYSQL_ROOT_PASSWORD=root
# Mercure
MERCURE_URL=http://mercure/.well-known/mercure
MERCURE_PUBLIC_URL=https://escapepage.com/.well-known/mercure
MERCURE_JWT_SECRET=55UtgFXsZu09TSTdeIA7ljK4HUo9DLkRzEB7MD5tqOLjRfAb
MERCURE_CORS_ALLOWED_ORIGINS=https://escapepage.com
MERCURE_TOPIC_BASE=https://escapepage.com
# Recaptcha
RECAPTCHA3_KEY=6LdIvk0sAAAAAC2jMbBXtjDQC24mmNbwHWBulxFu
RECAPTCHA3_SECRET=6LdIvk0sAAAAAE9TCGAQoczQFwR6l2dxkkwcPKsk

View File

@@ -1,20 +1,19 @@
services:
php:
environment:
XDEBUG_MODE: off
XDEBUG_MODE: "off"
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- mailer
# networks:
# backend:
# ipv4_address: 172.23.0.10
###> doctrine/doctrine-bundle ###
database:
ports:
- "3306"
###< doctrine/doctrine-bundle ###
###> doctrine/doctrine-bundle ###
###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
###> symfony/mailer ###
mailer:
image: axllent/mailpit
ports:
@@ -23,4 +22,17 @@ services:
environment:
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
# networks:
# backend:
# ipv4_address: 172.23.0.13
# networks:
# backend:
# name: escapepage_network
# driver: bridge
# ipam:
# config:
# - subnet: 172.23.0.0/16
# gateway: 172.23.0.1
# attachable: true
###< symfony/mailer ###

View File

@@ -1,4 +1,3 @@
version: '3.7'
services:
@@ -6,33 +5,65 @@ services:
build:
context: ..
dockerfile: docker/php/Dockerfile
args:
USER_ID: ${USER_ID}
GROUP_ID: ${GROUP_ID}
container_name: escapepage-php
volumes:
- ../:/var/www/html:delegated
- /etc/hosts:/etc/hosts:ro
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:
- database
- mercure
networks:
- backend
# networks:
# backend:
# ipv4_address: 172.23.0.10
restart: unless-stopped
php-worker:
build:
context: ..
dockerfile: docker/php/Dockerfile
args:
USER_ID: ${USER_ID}
GROUP_ID: ${GROUP_ID}
container_name: escapepage-php-worker
volumes:
- ../:/var/www/html:delegated
- /etc/hosts:/etc/hosts:ro
environment:
APP_ENV: 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:
- database
- mercure
command: ["php", "bin/console", "messenger:consume", "async", "-vv"]
networks:
- backend
# networks:
# backend:
# ipv4_address: 172.23.0.11
restart: unless-stopped
nginx:
@@ -40,13 +71,17 @@ services:
container_name: escapepage-nginx
ports:
- "8080:80"
- "8443:443"
volumes:
- ../:/var/www/html:ro
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- /etc/hosts:/etc/hosts:ro
depends_on:
- php
networks:
- backend
# networks:
# backend:
# ipv4_address: 172.23.0.12
restart: unless-stopped
mailer:
@@ -54,60 +89,75 @@ services:
container_name: escapepage-mailer
ports:
- "8025:8025"
networks:
- backend
volumes:
- /etc/hosts:/etc/hosts:ro
# networks:
# backend:
# ipv4_address: 172.23.0.13
restart: unless-stopped
mercure:
image: dunglas/mercure:v0.21
container_name: escapepage-mercure
environment:
SERVER_NAME: ":80"
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureJWTSignedBySymfonySecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureJWTSignedBySymfonySecretKey!'
MERCURE_CORS_ALLOWED_ORIGINS: http://localhost:8080
MERCURE_PUBLISH_ALLOWED_ORIGINS: http://localhost:8080
SERVER_NAME: "https://:443"
MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_SECRET}
MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_JWT_SECRET}
MERCURE_CORS_ALLOWED_ORIGINS: ${MERCURE_CORS_ALLOWED_ORIGINS}
MERCURE_PUBLISH_ALLOWED_ORIGINS: ${MERCURE_CORS_ALLOWED_ORIGINS}
MERCURE_EXTRA_DIRECTIVES: |
cors_origins http://localhost:8080
# Allow anonymous subscribers in dev only
cors_origins ${MERCURE_CORS_ALLOWED_ORIGINS}
publish_origins ${MERCURE_CORS_ALLOWED_ORIGINS}
anonymous
ports:
- "8090:80"
networks:
- backend
- "8090:443"
volumes:
- /etc/hosts:/etc/hosts:ro
- ./nginx/ssl/server.crt:/etc/caddy/certs/server.crt:ro
- ./nginx/ssl/server.key:/etc/caddy/certs/server.key:ro
# networks:
# backend:
# ipv4_address: 172.23.0.14
restart: unless-stopped
###> doctrine/doctrine-bundle ###
###> doctrine/doctrine-bundle ###
database:
image: mysql:8.0
container_name: escapepage-db
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE:-app}
MYSQL_USER: ${MYSQL_USER:-app}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-!ChangeMe!}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
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
timeout: 5s
retries: 10
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:
- 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.)
# ports:
# - "3306:3306"
networks:
- backend
ports:
- "3306:3306"
# networks:
# backend:
# ipv4_address: 172.23.0.15
restart: unless-stopped
###< doctrine/doctrine-bundle ###
volumes:
###> doctrine/doctrine-bundle ###
database_data:
###< doctrine/doctrine-bundle ###
networks:
backend:
driver: bridge
# networks:
# backend:
# name: escapepage_network
# driver: bridge
# ipam:
# config:
# - subnet: 172.23.0.0/16
# gateway: 172.23.0.1
# attachable: true

View File

@@ -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;

View File

@@ -1,6 +1,18 @@
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name _;
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
root /var/www/html/public;
index index.php index.html;
@@ -18,6 +30,14 @@ server {
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_pass php:9000;
fastcgi_read_timeout 120;
# Ensure HTTPS is correctly detected by Symfony if Nginx is behind a TLS termination proxy
fastcgi_param HTTPS $https if_not_empty;
# Standard forwarded headers
fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;
fastcgi_param HTTP_X_FORWARDED_PROTO $scheme;
fastcgi_param HTTP_X_FORWARDED_HOST $host;
fastcgi_param HTTP_X_FORWARDED_PORT $server_port;
}
location ~ /\.ht {

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -6,15 +6,32 @@ RUN apk add --no-cache \
git \
icu-dev \
libzip-dev \
libxml2-dev \
oniguruma-dev \
g++ \
make \
nodejs \
npm
npm \
shadow
# Install PHP extension installer
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
# Install PHP extensions
RUN docker-php-ext-configure intl \
&& docker-php-ext-install -j$(nproc) intl pdo pdo_mysql opcache zip
RUN install-php-extensions \
intl \
pdo_mysql \
opcache \
zip \
tokenizer \
ctype \
iconv \
mbstring \
dom \
xml \
simplexml \
xmlreader \
xmlwriter
# Install composer
ENV COMPOSER_ALLOW_SUPERUSER=1 \
@@ -24,7 +41,23 @@ COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Configure PHP
COPY docker/php/php.ini $PHP_INI_DIR/conf.d/zz-custom.ini
# 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
# Set permissions for Symfony directories
RUN mkdir -p var/cache var/log var/sessions && \
chown -R www-data:www-data var
# Default command
CMD ["php-fpm"]

View File

@@ -7,3 +7,8 @@ opcache.enable=1
opcache.enable_cli=1
opcache.validate_timestamps=1
opcache.revalidate_freq=0
log_errors=On
error_log=/var/www/html/var/log/errorlog_php.log
session.gc_maxlifetime=1440
session.cookie_lifetime=0

29
docker/restart.sh Normal file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
# Script to completely restart the project as requested
# Can be run from any directory
DOCKER_DIR=$(cd "$(dirname "$0")" && pwd)
ROOT_DIR=$(cd "$DOCKER_DIR/.." && pwd)
echo "Stopping and removing containers..."
(cd "$DOCKER_DIR" && docker compose -f compose.yaml -f compose.override.yaml down -v --remove-orphans) || true
docker network rm escapepage_network || true
docker network rm $(docker network ls -q --filter name=escapepage) || true
docker network prune -f || true
docker rm -f escapepage-db escapepage-php escapepage-nginx escapepage-mercure escapepage-mailer escapepage-php-worker || true
docker system prune -f || true
echo "Clearing Docker build cache..."
docker builder prune -af
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" "$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

View File

@@ -17,18 +17,18 @@ set -euo pipefail
ROOT_DIR=$(cd "$(dirname "$0")"/.. && pwd)
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
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' was found. Please install Docker Compose." >&2
echo "Error: Neither 'docker-compose' nor 'docker compose' was found. Please install Docker Compose." >&2
exit 1
fi
# Helper to run docker compose from the docker/ directory
dc() { (cd "$DOCKER_DIR" && $DOCKER_COMPOSE -f compose.yaml "$@"); }
# Helper to run docker compose from the docker directory
dc() { (cd "$DOCKER_DIR" && $DOCKER_COMPOSE -f compose.yaml -f compose.override.yaml --env-file ../.env "$@"); }
REBUILD=1
RECREATE=0
@@ -61,7 +61,7 @@ if [ "$RECREATE" -eq 1 ]; then
fi
# Start stack
dc up "${BUILD_ARGS[@]}"
dc up -d "${BUILD_ARGS[@]}"
# Helper to run commands in php container
pexec() { dc exec -T php "$@"; }
@@ -104,9 +104,17 @@ fi
# Prepare DB
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..."
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)
if [ -f "$ROOT_DIR/importmap.php" ]; then

View File

@@ -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');
}
}

View File

@@ -1,9 +1,18 @@
<?php
use App\Kernel;
use Symfony\Component\HttpFoundation\Request;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
if ($trustedProxies = $context['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) {
Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PREFIX);
}
if ($trustedHosts = $context['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) {
Request::setTrustedHosts([$trustedHosts]);
}
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};

View File

@@ -31,7 +31,7 @@ class ActivationController extends AbstractController
try {
$emailVerifier->handleEmailConfirmation($request, $user);
} catch (VerifyEmailExceptionInterface $exception) {
$this->addFlash('error', $exception->getReason());
$this->addFlash('error', $exception->getReason() . ' If the link has expired, you can <a href="' . $this->generateUrl('app_verify_resend_email') . '">request a new one here</a>.');
return $this->redirectToRoute('app_register');
}

View File

@@ -4,6 +4,8 @@ namespace App\Tech\Controller;
use App\Tech\Entity\User;
use App\Tech\Form\RegistrationFormType;
use App\Tech\Form\ResendVerificationEmailFormType;
use App\Tech\Repository\UserRepository;
use App\Tech\Service\EmailVerifier;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
@@ -39,7 +41,7 @@ class RegistrationController extends AbstractController
// generate a signed url and email it to the user
$emailVerifier->sendEmailConfirmation('app_verify_email', $user,
(new TemplatedEmail())
->from('noreply@escapepage.dev')
->from($this->getParameter('mailer_from'))
->to($user->getEmail())
->subject('Please Confirm your Email')
->htmlTemplate('tech/registration/confirmation_email.html.twig')
@@ -54,4 +56,41 @@ class RegistrationController extends AbstractController
'registrationForm' => $form->createView(),
]);
}
#[Route('/verify/resend', name: 'app_verify_resend_email')]
public function resendVerificationEmail(Request $request, UserRepository $userRepository, EmailVerifier $emailVerifier): Response
{
$form = $this->createForm(ResendVerificationEmailFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$email = $form->get('email')->getData();
$user = $userRepository->findOneBy(['email' => $email]);
if ($user) {
if (!$user->isVerified()) {
$emailVerifier->sendEmailConfirmation('app_verify_email', $user,
(new TemplatedEmail())
->from($this->getParameter('mailer_from'))
->to($user->getEmail())
->subject('Please Confirm your Email')
->htmlTemplate('tech/registration/confirmation_email.html.twig')
);
} else {
$this->addFlash('info', 'This email address is already verified.');
return $this->redirectToRoute('app_login');
}
}
// We show the same success message regardless of whether the user exists or not,
// to avoid revealing whether an email is registered.
$this->addFlash('success', 'If an account exists with this email, a new confirmation link has been sent.');
return $this->redirectToRoute('website_home');
}
return $this->render('tech/registration/resend_verification_email.html.twig', [
'resendForm' => $form->createView(),
]);
}
}

View File

@@ -26,15 +26,13 @@ class ChangePasswordFormType extends AbstractType
],
'first_options' => [
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
]),
new Length([
'min' => 12,
'minMessage' => 'Your password should be at least {{ limit }} characters',
new NotBlank(message: 'Please enter a password'),
new Length(
min: 12,
minMessage: 'Your password should be at least {{ limit }} characters',
// max length allowed by Symfony for security reasons
'max' => 4096,
]),
max: 4096,
),
new PasswordStrength(),
new NotCompromisedPassword(),
],

View File

@@ -3,6 +3,8 @@
namespace App\Tech\Form;
use App\Tech\Entity\User;
use Karser\Recaptcha3Bundle\Form\Recaptcha3Type;
use Karser\Recaptcha3Bundle\Validator\Constraints\Recaptcha3;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -21,9 +23,7 @@ class RegistrationFormType extends AbstractType
->add('email', EmailType::class)
->add('username', TextType::class, [
'constraints' => [
new NotBlank([
'message' => 'Please enter a username',
]),
new NotBlank(message: 'Please enter a username'),
],
])
->add('plainPassword', RepeatedType::class, [
@@ -33,17 +33,19 @@ class RegistrationFormType extends AbstractType
'first_options' => ['label' => 'Password'],
'second_options' => ['label' => 'Repeat Password'],
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
]),
new Length([
'min' => 6,
'minMessage' => 'Your password should be at least {{ limit }} characters',
new NotBlank(message: 'Please enter a password'),
new Length(
min: 6,
minMessage: 'Your password should be at least {{ limit }} characters',
// max length allowed by Symfony for security reasons
'max' => 4096,
]),
max: 4096,
),
],
])
->add('captcha', Recaptcha3Type::class, [
'constraints' => new Recaptcha3(),
'action_name' => 'registration',
])
;
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Tech\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
class ResendVerificationEmailFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('email', EmailType::class, [
'constraints' => [
new NotBlank([
'message' => 'Please enter your email',
]),
new Email([
'message' => 'Please enter a valid email address',
]),
],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([]);
}
}

View File

@@ -16,9 +16,7 @@ class ResetPasswordRequestFormType extends AbstractType
->add('email', EmailType::class, [
'attr' => ['autocomplete' => 'email'],
'constraints' => [
new NotBlank([
'message' => 'Please enter your email',
]),
new NotBlank(message: 'Please enter your email'),
],
])
;

View File

@@ -43,7 +43,7 @@ class EmailVerifier
*/
public function handleEmailConfirmation(Request $request, User $user): void
{
$this->verifyEmailHelper->validateEmailConfirmation($request->getUri(), (string) $user->getId(), $user->getEmail());
$this->verifyEmailHelper->validateEmailConfirmationFromRequest($request, (string) $user->getId(), $user->getEmail());
$user->setIsVerified(true);

View File

@@ -3,6 +3,7 @@
namespace App\Tech\Service;
use App\Tech\Entity\User;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
@@ -16,7 +17,7 @@ class UserChecker implements UserCheckerInterface
}
if (!$user->isVerified()) {
throw new CustomUserMessageAuthenticationException('Your email address is not verified.');
throw new CustomUserMessageAuthenticationException('Your email address is not verified.', ['%resend_link%' => '/verify/resend']);
}
}

View File

@@ -35,6 +35,18 @@
"migrations/.gitignore"
]
},
"karser/karser-recaptcha3-bundle": {
"version": "0.3",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "0.1",
"ref": "c51ce07c10331d506762efe25b6f5843c1a5ea17"
},
"files": [
"./config/packages/karser_recaptcha3.yaml"
]
},
"phpunit/phpunit": {
"version": "11.5",
"recipe": {

View File

@@ -4,6 +4,7 @@
Please confirm your email address by clicking the following link: <br><br>
<a href="{{ signedUrl }}">Confirm my Email</a>.
This link will expire in {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}.
If the link has expired or doesn't work, you can <a href="{{ url('app_verify_resend_email') }}">request a new one</a>.
</p>
<p>

View File

@@ -11,6 +11,7 @@
{{ form_row(registrationForm.email) }}
{{ form_row(registrationForm.username) }}
{{ form_row(registrationForm.plainPassword) }}
{{ form_row(registrationForm.captcha) }}
<button type="submit" class="btn">Register</button>
{{ form_end(registrationForm) }}

View File

@@ -0,0 +1,17 @@
{% extends 'base.html.twig' %}
{% block title %}Resend verification email{% endblock %}
{% block body %}
<h1>Resend verification email</h1>
<p>Enter your email address and we will send you a new link to verify your account.</p>
{{ form_errors(resendForm) }}
{{ form_start(resendForm) }}
{{ form_row(resendForm.email) }}
<button type="submit" class="btn">Resend email</button>
{{ form_end(resendForm) }}
{% endblock %}

View File

@@ -5,7 +5,12 @@
{% block body %}
<form method="post">
{% if error %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
<div class="alert alert-danger">
{{ error.messageKey|trans(error.messageData, 'security')|raw }}
{% if error.messageData['%resend_link%'] is defined %}
<a href="{{ error.messageData['%resend_link%'] }}">Resend activation link</a>
{% endif %}
</div>
{% endif %}
{% if app.user %}
@@ -41,5 +46,8 @@
<div class="mt-3">
<a href="{{ path('app_forgot_password_request') }}">Forgot your password?</a>
</div>
<div class="mt-1">
<a href="{{ path('app_verify_resend_email') }}">Didn't receive activation email?</a>
</div>
</form>
{% endblock %}