Compare commits
102 Commits
c0aa2ad44e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a9c0f0052 | ||
|
|
57af01ea6f | ||
|
|
4b0f8fb5c6 | ||
|
|
a68eeb3ac6 | ||
|
|
cc6c6a6456 | ||
|
|
184a5ff0de | ||
|
|
170ae2ce43 | ||
|
|
16625c557c | ||
|
|
a15edfabed | ||
| 66a682f69f | |||
|
|
576d86f602 | ||
|
|
891c1b4ecb | ||
|
|
f5bac0fe2d | ||
|
|
0276199488 | ||
|
|
83381ef57f | ||
|
|
c6153c62f1 | ||
|
|
4f7471ab0f | ||
|
|
9237d9ad49 | ||
|
|
a3573d5a09 | ||
|
|
5e87ae90d8 | ||
|
|
05a514bad6 | ||
|
|
3a34266461 | ||
|
|
7fe8f9322a | ||
|
|
27827bd2a9 | ||
|
|
de1c6f4ed2 | ||
|
|
a6df6cbf0c | ||
|
|
a90489da28 | ||
|
|
498ba1bfca | ||
|
|
f96e51420f | ||
|
|
f3bf472bc6 | ||
|
|
34d89129ae | ||
|
|
0c0c71c7b4 | ||
|
|
d70ef9282e | ||
|
|
de67d95d4f | ||
|
|
bcb42a27b8 | ||
|
|
bc4f7a8c79 | ||
|
|
3b98e8650b | ||
|
|
09f9abcfb8 | ||
|
|
3b3cb69aa7 | ||
|
|
badca1af53 | ||
|
|
b649d48250 | ||
|
|
0e3a3992fa | ||
|
|
cfac6d10ec | ||
|
|
a10ad7de58 | ||
|
|
f810ad07b7 | ||
|
|
41f3547f6f | ||
|
|
09b7e78fdd | ||
|
|
47091cd4e3 | ||
|
|
ac4c5ef261 | ||
|
|
490f730c97 | ||
|
|
73d6ea478c | ||
|
|
9da6a60dbe | ||
|
|
7ca0bec145 | ||
|
|
ea54c87426 | ||
|
|
12e87edc4d | ||
|
|
85416f5a07 | ||
|
|
2f81a60ff7 | ||
|
|
0e27217ab4 | ||
|
|
b3531b5d7c | ||
|
|
239b1e136a | ||
|
|
db04cafcb1 | ||
|
|
5ce7e15565 | ||
|
|
df12c4bd11 | ||
|
|
769d8b4e74 | ||
|
|
14871336a3 | ||
|
|
3604c63940 | ||
| 7257c51bdf | |||
|
|
641573842c | ||
| abc3712d97 | |||
|
|
66cb356955 | ||
|
|
ffd20f5535 | ||
| 1b52f80448 | |||
|
|
b965f0f085 | ||
|
|
c4c989db4c | ||
| 37507bd169 | |||
|
|
732148a533 | ||
|
|
50d7ce745c | ||
| a475c1a268 | |||
|
|
1ae0f651d8 | ||
|
|
8e933631b2 | ||
| f8746207e6 | |||
|
|
e42966e618 | ||
|
|
e4976e51fa | ||
|
|
65070688ec | ||
|
|
e54c870f05 | ||
| f5aabafcc5 | |||
|
|
de4b7bca6a | ||
| c6adb00219 | |||
|
|
5f6ea89179 | ||
|
|
c384fc3dd5 | ||
|
|
78b570bd75 | ||
| 74f52db002 | |||
|
|
56590a901f | ||
| 49045bc696 | |||
|
|
28ee969c29 | ||
|
|
7230520551 | ||
|
|
59c1d97d84 | ||
|
|
3de1e907f2 | ||
| 91c0c3e6d1 | |||
|
|
8e3807e079 | ||
|
|
10c3dbc066 | ||
|
|
af13be2196 |
25
.dockerignore
Normal file
25
.dockerignore
Normal 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
|
||||||
50
.env
50
.env
@@ -15,17 +15,30 @@
|
|||||||
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
APP_ENV=dev
|
APP_ENV=prod
|
||||||
APP_SECRET=
|
APP_SECRET=695679907f9c3818e6924d547f872651
|
||||||
|
TRUSTED_PROXIES=127.0.0.1,172.20.0.1,172.20.0.0/16
|
||||||
|
TRUSTED_HOSTS=^.*$
|
||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
SITE_BASE_URL=https://escapepage.com
|
||||||
|
|
||||||
###> doctrine/doctrine-bundle ###
|
###> doctrine/doctrine-bundle ###
|
||||||
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
# 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
|
# 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="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://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 ###
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
###> symfony/messenger ###
|
###> symfony/messenger ###
|
||||||
@@ -37,14 +50,13 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
|||||||
|
|
||||||
###> symfony/mailer ###
|
###> symfony/mailer ###
|
||||||
# Development: use Mailpit (docker compose override provides service `mailer` on port 1025)
|
# 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):
|
# Production/Stage (uncomment and set SENDGRID_API_KEY in real env or secrets):
|
||||||
# MAILER_DSN=sendgrid+api://%env(SENDGRID_API_KEY)%
|
# MAILER_DSN=sendgrid+api://%env(SENDGRID_API_KEY)%
|
||||||
# Alternatively, via SMTP (no extra package needed):
|
# Alternatively, via SMTP (no extra package needed):
|
||||||
# MAILER_DSN="smtp://apikey:%env(SENDGRID_API_KEY)%@smtp.sendgrid.net:587?encryption=tls"
|
# 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):
|
# 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/mailer ###
|
||||||
|
|
||||||
###> symfony/sendgrid-mailer ###
|
###> symfony/sendgrid-mailer ###
|
||||||
@@ -53,11 +65,27 @@ MAILER_DSN=smtp://mailer:1025
|
|||||||
|
|
||||||
###> mercure ###
|
###> mercure ###
|
||||||
# Internal hub URL used by the PHP app (reachable from the php container)
|
# 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
|
# 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.
|
# Shared secret for signing JWTs (dev only). In prod, set via real env/secrets.
|
||||||
MERCURE_JWT_SECRET=!ChangeThisMercureJWT!
|
MERCURE_JWT_SECRET=!ChangeThisMercureJWTSignedBySymfonySecretKey!
|
||||||
# Base URL for Mercure topics. Use .dev in development; override to .com in prod via .env.prod or real env.
|
# Pre-generated JWT tokens for convenience
|
||||||
MERCURE_TOPIC_BASE=https://escapepage.dev
|
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 ###
|
###< 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 ###
|
||||||
|
|||||||
13
.env.dev
13
.env.dev
@@ -2,3 +2,16 @@
|
|||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
APP_SECRET=620e9ce5f88a714b636179eb39d5be4f
|
APP_SECRET=620e9ce5f88a714b636179eb39d5be4f
|
||||||
###< symfony/framework-bundle ###
|
###< 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"
|
||||||
|
|||||||
36
.env.prod
36
.env.prod
@@ -1,9 +1,37 @@
|
|||||||
|
APP_ENV=prod
|
||||||
|
APP_SECRET=a8f89e179e8c338423697669d6728c2c
|
||||||
|
|
||||||
### Compiled or real environment variables should be used in production.
|
### Compiled or real environment variables should be used in production.
|
||||||
### Configure MAILER_DSN to use SendGrid API transport.
|
### Configure MAILER_DSN to use SendGrid API transport.
|
||||||
### Prefer storing SENDGRID_API_KEY using Symfony Secrets or real env vars.
|
### Prefer storing SENDGRID_API_KEY using Symfony Secrets or real env vars.
|
||||||
|
|
||||||
###> symfony/mailer ###
|
###> symfony/mailer ###
|
||||||
# Example using SendGrid API key (replace with real secret via vault/secrets):
|
MAILER_DSN=sendgrid://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@default
|
||||||
# SENDGRID_API_KEY=SG.xxxxx
|
MAILER_FROM=mailer@escapepage.nl
|
||||||
MAILER_DSN=sendgrid+api://%env(resolve:SENDGRID_API_KEY)%@default
|
|
||||||
###< symfony/mailer ###
|
###< 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 ###
|
||||||
|
|||||||
15
.env.test
15
.env.test
@@ -1,3 +1,18 @@
|
|||||||
|
APP_ENV=test
|
||||||
|
|
||||||
# define your env variables for the test env here
|
# define your env variables for the test env here
|
||||||
KERNEL_CLASS='App\Kernel'
|
KERNEL_CLASS='App\Kernel'
|
||||||
APP_SECRET='$ecretf0rt3st'
|
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"
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,6 +6,9 @@
|
|||||||
/config/secrets/prod/prod.decrypt.private.php
|
/config/secrets/prod/prod.decrypt.private.php
|
||||||
/public/bundles/
|
/public/bundles/
|
||||||
/var/
|
/var/
|
||||||
|
!/var/volumes/
|
||||||
|
/var/volumes/*
|
||||||
|
!/var/volumes/.gitignore
|
||||||
/vendor/
|
/vendor/
|
||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
|||||||
1
.idea/escapepage.iml
generated
1
.idea/escapepage.iml
generated
@@ -140,6 +140,7 @@
|
|||||||
<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" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|||||||
3
.idea/php.xml
generated
3
.idea/php.xml
generated
@@ -153,6 +153,7 @@
|
|||||||
<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" />
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
|
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
|
||||||
@@ -177,4 +178,4 @@
|
|||||||
<component name="PsalmOptionsConfiguration">
|
<component name="PsalmOptionsConfiguration">
|
||||||
<option name="transferred" value="true" />
|
<option name="transferred" value="true" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
14
README.md
14
README.md
@@ -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`
|
||||||
@@ -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)
|
## Email (Mailpit in dev, SendGrid 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 SendGrid.
|
||||||
- Require package (already in composer): `symfony/sendgrid-mailer`.
|
- 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 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 +91,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:
|
||||||
|
|||||||
212
assets/game1.js
212
assets/game1.js
@@ -1,7 +1,10 @@
|
|||||||
/* 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;
|
||||||
|
|
||||||
|
function subscribeToMercure(mercurePublicUrl, topic, myScreen) {
|
||||||
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 +13,33 @@ 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);
|
||||||
|
|
||||||
|
// data is [sendTo, message]
|
||||||
|
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) {
|
||||||
|
const msgEl = document.createElement('div');
|
||||||
|
msgEl.className = 'message';
|
||||||
|
msgEl.textContent = data[1];
|
||||||
|
msgEl.style.color = '#0F0'; // Green for incoming messages
|
||||||
|
msgEl.style.marginBottom = '10px';
|
||||||
|
messageContainer.appendChild(msgEl);
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
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 +55,28 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
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 || {});
|
||||||
@@ -60,6 +112,15 @@ 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
|
||||||
|
window.addEventListener('beforeunload', (event) => {
|
||||||
|
// Standard way to trigger the browser's confirmation dialog
|
||||||
|
event.preventDefault();
|
||||||
|
// Included for compatibility with older browsers
|
||||||
|
event.returnValue = '';
|
||||||
|
});
|
||||||
|
|
||||||
if (!cfgEl) {
|
if (!cfgEl) {
|
||||||
console.warn('[Mercure][game1] #mercure-config element not found on page');
|
console.warn('[Mercure][game1] #mercure-config element not found on page');
|
||||||
return;
|
return;
|
||||||
@@ -67,16 +128,64 @@ 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 userId = cfgEl.dataset.userId;
|
const apiCheckFinishedUrl = cfgEl.dataset.apiCheckFinishedUrl;
|
||||||
|
const lostUrl = cfgEl.dataset.lostUrl;
|
||||||
|
|
||||||
if (mercurePublicUrl && topic) {
|
if (mercurePublicUrl && topic) {
|
||||||
subscribeToMercure(mercurePublicUrl, topic);
|
subscribeToMercure(mercurePublicUrl, topic, screen);
|
||||||
} 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) {
|
||||||
|
window.location.href = 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) {
|
||||||
@@ -89,7 +198,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
if (apiEchoUrl) {
|
if (apiEchoUrl) {
|
||||||
const echo = await fetchJson(apiEchoUrl, {
|
const echo = await fetchJson(apiEchoUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { message: 'from game1.js', ts: new Date().toISOString(), user: userId },
|
body: { message: 'from game1.js', ts: new Date().toISOString() },
|
||||||
});
|
});
|
||||||
console.log('[API][game1] echo →', echo);
|
console.log('[API][game1] echo →', echo);
|
||||||
} else {
|
} else {
|
||||||
@@ -98,4 +207,99 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[API][game1] Request failed:', e);
|
console.error('[API][game1] Request failed:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add messages to message-container
|
||||||
|
const messageContainer = document.getElementById('message-container');
|
||||||
|
if (messageContainer) {
|
||||||
|
let messages = [
|
||||||
|
['System initializing...', 500],
|
||||||
|
['Connection established.', 200],
|
||||||
|
['Welcome agent to the mainframe.', 1000],
|
||||||
|
['Scanning...', 3000],
|
||||||
|
['Virus detected.', 500],
|
||||||
|
['Starting Mainframe help modus...', 2000],
|
||||||
|
['Help modus activated.', 500],
|
||||||
|
['Blocking virus activated', 0]
|
||||||
|
];
|
||||||
|
|
||||||
|
let currentMessageIndex = 0;
|
||||||
|
|
||||||
|
const printNextMessage = () => {
|
||||||
|
if (currentMessageIndex < messages.length) {
|
||||||
|
|
||||||
|
const msg = messages[currentMessageIndex];
|
||||||
|
const msgEl = document.createElement('div');
|
||||||
|
|
||||||
|
let extraClass = '';
|
||||||
|
if(msg[2])
|
||||||
|
extraClass = msg[2];
|
||||||
|
|
||||||
|
msgEl.className = 'message ' + extraClass;
|
||||||
|
msgEl.textContent = msg[0];
|
||||||
|
msgEl.style.marginBottom = '10px';
|
||||||
|
messageContainer.appendChild(msgEl);
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
|
||||||
|
playSound();
|
||||||
|
|
||||||
|
currentMessageIndex++;
|
||||||
|
setTimeout(printNextMessage, msg[1]);
|
||||||
|
if (sequenceFinished) {
|
||||||
|
flashRed();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// After it has printed a set of messages, it has to start a timer of 2 seconds
|
||||||
|
console.log('[Game1] All messages printed. Starting 2s timer to expand message-container height...');
|
||||||
|
setTimeout(() => {
|
||||||
|
messageContainer.style.height = '400vh';
|
||||||
|
const inputField = document.getElementById('input-message');
|
||||||
|
inputField.disabled = false;
|
||||||
|
|
||||||
|
// Add event listener for Enter key
|
||||||
|
inputField.addEventListener('keypress', async (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
stillPlayingSound = false;
|
||||||
|
sequenceFinished = false;
|
||||||
|
const message = inputField.value.trim();
|
||||||
|
|
||||||
|
const msgEl = document.createElement('div');
|
||||||
|
msgEl.className = 'message';
|
||||||
|
msgEl.textContent = message;
|
||||||
|
msgEl.style.marginBottom = '10px';
|
||||||
|
messageContainer.appendChild(msgEl);
|
||||||
|
|
||||||
|
if (message && apiEchoUrl) {
|
||||||
|
inputField.value = '';
|
||||||
|
try {
|
||||||
|
const response = await fetchJson(apiEchoUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { message, ts: new Date().toISOString() },
|
||||||
|
});
|
||||||
|
console.log('[API][game1] message sent →', response);
|
||||||
|
if (response && response.result && Array.isArray(response.result.result)) {
|
||||||
|
response.result.result.forEach(text => {
|
||||||
|
const msgEl = document.createElement('div');
|
||||||
|
msgEl.className = 'message';
|
||||||
|
msgEl.textContent = text;
|
||||||
|
msgEl.style.marginBottom = '10px';
|
||||||
|
messageContainer.appendChild(msgEl);
|
||||||
|
});
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[API][game1] Failed to send message:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[Game1] message-container height changed to 400vh and input enabled');
|
||||||
|
sequenceFinished = true;
|
||||||
|
console.log('[Game1] sequenceFinished is now TRUE');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
printNextMessage();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
10
assets/game1/filesystem/var/rapports/001_145-93.txt
Normal file
10
assets/game1/filesystem/var/rapports/001_145-93.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Undercover Report – Case File 145-93
|
||||||
|
Agent: Carver (cover identity)
|
||||||
|
Date: 02/18 – 02/22
|
||||||
|
|
||||||
|
Subject operates a rural safehouse rumored to shelter fugitives and serve as a transient waypoint. Agent’s cover as a drifter seeking work provided entry in exchange for chores. On 02/19, visitors with gang tattoos arrived after midnight and were housed in separate rooms. Vehicles were parked under tree cover, and license plates were mud‑smeared. On 02/20, the subject warned of “new heat in town” and began burning scrap papers in a barrel.
|
||||||
|
|
||||||
|
Later that night, a duffel of documents was left unattended. The agent briefly photographed maps with marked back‑road routes and coded notes. The handwriting matches samples from a related case. On 02/21, the subject probed the agent’s story with pointed questions about prior arrests. Cover held after the agent recited the rehearsed backstory.
|
||||||
|
|
||||||
|
Cover credible but time‑limited. Recommend immediate analysis of the photographed maps, ID of transient visitors via tattoos and vehicle features, and a synchronized warrant service before the location rotates to a fresh site.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
11
assets/game1/filesystem/var/rapports/007_19-52.txt
Normal file
11
assets/game1/filesystem/var/rapports/007_19-52.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Undercover Report – Case File 19-52
|
||||||
|
Agent: Harper (cover identity)
|
||||||
|
Date: 07/22 – 07/26
|
||||||
|
|
||||||
|
Subject controls a pawnshop that exhibits transaction volumes and cash handling inconsistent with normal retail activity. Cover identity as electronics repair tech granted backroom access to the testing bench, DVR cabinet, and inventory cage. On 07/23, the subject hosted a closed‑door meeting with four associates who arrived separately and staggered departures. Through the vent grate, agent heard references to “offshore wires,” “clean accounts,” and “quarter‑end flush.” Subject maintained a spiral ledger with denominations recorded in columns that do not match typical point‑of‑sale exports.
|
||||||
|
|
||||||
|
On 07/24, the subject ordered cameras repositioned to avoid capturing the safe directly. Agent was subtly tested regarding loyalty—assigned an after‑hours soldering task while the subject counted bundled currency on the shop floor. Later that night, an unmarked van delivered two duffel bags that were moved to the vault without intake paperwork. Subject personally signed a receipt on blank stationery and pocketed the copy.
|
||||||
|
|
||||||
|
On 07/25, subject complained about “Treasury heat” and instructed the bookkeeper to route payments through two new accounts, both named with common surnames. Names did not match any employee records. Agent retrieved partial account numbers from a discarded note.
|
||||||
|
|
||||||
|
Cover remains credible. Recommend immediate financial task‑force review of the ledger, subpoena of the newly referenced accounts, and discreet license‑plate canvass for the van. Surveillance continuity is advised; subject exhibits rising paranoia yet continues to centralize cash at the location.
|
||||||
10
assets/game1/filesystem/var/rapports/011_130-62.txt
Normal file
10
assets/game1/filesystem/var/rapports/011_130-62.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Undercover Report – Case File 130-62
|
||||||
|
Agent: Lennox (cover identity)
|
||||||
|
Date: 12/02 – 12/06
|
||||||
|
|
||||||
|
Subject runs an art gallery suspected of laundering through inflated valuations and forged provenance. Agent embedded as gallery assistant. On 12/03, the subject finalized a cash sale for a painting at five times market value to an anonymous buyer. On 12/04, the subject ordered an assistant to “adjust provenance papers,” instructing edits to dates and prior ownership. The font choice and printer bleed matched a stack of older certificates in a locked drawer.
|
||||||
|
|
||||||
|
On 12/05, a private viewing hosted foreign clients who avoided staff contact. One carried a briefcase later exchanged for a wrapped canvas in the loading alcove. On 12/06, the subject cautioned staff not to speak with authorities and appeared tense while counting cash in the office.
|
||||||
|
|
||||||
|
Cover secure. Recommend seizure of the forged documents, liaison with cultural‑property experts to authenticate inventory, and identification of the foreign clients via travel manifests and nearby CCTV.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
12
assets/game1/filesystem/var/rapports/012_94-31.txt
Normal file
12
assets/game1/filesystem/var/rapports/012_94-31.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Undercover Report – Case File 94-31
|
||||||
|
Agent: Donovan (cover identity)
|
||||||
|
Date: 06/01 – 06/05
|
||||||
|
|
||||||
|
Subject operates a private marina suspected of facilitating smuggling. Agent’s cover as a yacht mechanic provides daily access to slips and late‑night maintenance calls. On 06/02, the subject directed the agent to assist with unloading three sealed crates from a speedboat arriving past midnight. Crates bore generic “marine supplies” stickers; handlers treated them with unusual care. Opening was not feasible without tipping the crew.
|
||||||
|
|
||||||
|
On 06/03, a heated exchange with a subordinate revealed concerns over “late payments” from overseas. The subject warned that debts must be cleared “before customs comes sniffing.” The tone suggested reliance on offshore accounts. On 06/04, the subject introduced an associate known only as “Captain,” who exhibited military bearing and carried a sidearm; conversation centered on moving delivery windows to avoid routine patrol sweeps.
|
||||||
|
|
||||||
|
On 06/05, the subject carried a heavy duffel into the office and drew blinds for over an hour. Subsequent demeanor was relaxed, implying a successful cash reconciliation.
|
||||||
|
|
||||||
|
Cover intact. Recommend forensic review of marina accounting, financial tracing of suspected offshore links, and coordination with harbor patrol to flag vessels matching the speedboat’s profile.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
10
assets/game1/filesystem/var/rapports/016_98-07.txt
Normal file
10
assets/game1/filesystem/var/rapports/016_98-07.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Undercover Report – Case File 98-07
|
||||||
|
Agent: Fletcher (cover identity)
|
||||||
|
Date: 07/11 – 07/14
|
||||||
|
|
||||||
|
Subject serves as a lieutenant managing a distribution hub inside an abandoned textile mill. Cover identity as a shell‑company delivery driver grants entrance to the loading dock and elevator cages. On 07/12, the subject assigned the agent to ferry sealed packages to a storage‑locker complex registered under a false name. Package weight and precautions suggest narcotics. Keys on a ring were photographed; several cuts matched lockers in Row C.
|
||||||
|
|
||||||
|
On 07/13, the subject referenced “Chicago buyers,” instructing the crew to increase output despite law‑enforcement chatter. Morale appeared strained. On 07/14, the subject gathered the crew, delivered a loyalty speech about “traitors,” and scanned faces while the enforcer lingered by the door. No direct challenge to the agent, but suspicion is rising.
|
||||||
|
|
||||||
|
Cover holds but the environment is volatile. Recommend warrants for the locker complex keyed to the photographed cuts, discreet traffic stops on the distribution couriers, and a contingency extraction plan should violence erupt at the mill.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
10
assets/game1/filesystem/var/rapports/017_123-88.txt
Normal file
10
assets/game1/filesystem/var/rapports/017_123-88.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Undercover Report – Case File 123-88
|
||||||
|
Agent: Holloway (cover identity)
|
||||||
|
Date: 11/04 – 11/08
|
||||||
|
|
||||||
|
Subject leverages a regional trucking firm to move contraband under cover of legitimate freight. Agent’s cover as a dispatcher assistant enables access to GPS routes and driver notes. On 11/05, routing orders labeled “detour deliveries” consistently bypassed weigh stations, and manifests showed weight mismatches. On 11/06, the subject instructed a driver to deliver “straight to the mountain cabin,” providing coordinates later confirmed by GPS ping.
|
||||||
|
|
||||||
|
On 11/07, news of law‑enforcement activity near the interstate caused the subject to cancel multiple runs. Crew anxiety rose. On 11/08, the subject praised the agent for rerouting a truck around a checkpoint, further legitimizing the cover.
|
||||||
|
|
||||||
|
Cover intact. Recommend aerial survey of the coordinates to identify storage, placement of covert trackers on the detour fleet, and financial review of the dispatch accounts to map the laundering flow.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
11
assets/game1/filesystem/var/rapports/019_31-11.txt
Normal file
11
assets/game1/filesystem/var/rapports/019_31-11.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Undercover Report – Case File 31-11
|
||||||
|
Agent: Vega (cover identity)
|
||||||
|
Date: 09/04 – 09/07
|
||||||
|
|
||||||
|
Subject runs a rural pharmacy with unusually high order volumes for specific controlled substances. Agent’s cover as a delivery technician enabled access to loading doors and packing slips. On 09/05, the subject accepted a pallet of blister packs outside normal receiving hours, directing it straight to a side room. Labels were legitimate, but lot numbers did not appear in the chain‑of‑custody app used elsewhere in the store. Staff avoided eye contact and deferred to the subject for every deviation.
|
||||||
|
|
||||||
|
On 09/06, two sedan drivers arrived within minutes of each other, both collecting “returns” in sealed totes. Agent observed the subject bypassing the returns register and printing generic labels from a stand‑alone thermal unit. Drivers refused signatures and left via the alley. The subject later shredded a stack of thermal drafts without balancing the counts.
|
||||||
|
|
||||||
|
On 09/07, agent overheard a phone call in which the subject referenced “doctor packs,” “holiday traffic,” and “quota pressure.” The cadence suggested coordination with a prescriber and an external distributor. The subject’s mood shifted sharply after the call; staff were told to “keep it to cash only” for the remainder of the day.
|
||||||
|
|
||||||
|
Cover remains viable. Recommend a joint audit with state pharmacy regulators, review of the lot numbers observed, and controlled buys through the returns channel to document diversion.
|
||||||
12
assets/game1/filesystem/var/rapports/031_53-28.txt
Normal file
12
assets/game1/filesystem/var/rapports/031_53-28.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Undercover Report – Case File 53-28
|
||||||
|
Agent: Navarro (cover identity)
|
||||||
|
Date: 02/01 – 02/04
|
||||||
|
|
||||||
|
Subject owns a private courier service that advertises same‑day delivery for tech firms. Shipment profiles and driver chatter suggest the operation moves high‑end electronics and crypto‑mining components off the books. Agent’s cover as a dispatch assistant provided map access and driver rotations. On 02/02, the subject created a route labeled “white run” that bypassed weigh stations and used alley handoffs. Driver later returned with a sealed envelope and no signature trail.
|
||||||
|
|
||||||
|
On 02/03, a pallet bearing unfamiliar consignee codes was loaded into a van with interior panels removed—consistent with contraband concealment. Subject lectured staff about “never opening the blue totes,” then carried one into a locked server closet. Agent heard a fan spin‑up and a brief data sync tone from a nearby laptop, implying a quick‑clone procedure.
|
||||||
|
|
||||||
|
On 02/04, the subject hinted the agent could “graduate to vest access” if weekend performance stayed quiet. A senior driver mentioned “hash boards” failing in transit and needing replacements before “the buyer from Reno flies out.”
|
||||||
|
|
||||||
|
Cover stable. Recommend covert GPS beacons on the white‑run vans, subpoena of the courier’s API logs for the locked server closet, and coordination with air‑cargo teams for the Reno timeline.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
10
assets/game1/filesystem/var/rapports/063_117-56.txt
Normal file
10
assets/game1/filesystem/var/rapports/063_117-56.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Undercover Report – Case File 117-56
|
||||||
|
Agent: O’Connor (cover identity)
|
||||||
|
Date: 10/15 – 10/18
|
||||||
|
|
||||||
|
Subject operates an illegal backroom poker circuit that attracts mid‑tier executives and local power brokers. Agent’s cover as a wealthy gambler provided entry. On 10/16, buy‑ins far exceeded legal thresholds, and the subject boasted of protection from “friends in city hall.”
|
||||||
|
|
||||||
|
On 10/17, an argument erupted between the subject and an enforcer over unpaid debts. Threats were made, but play resumed. On 10/18, the subject introduced an associate known as “Silver,” carrying a distinctive lighter described in other investigations. Presence of Silver indicates links to a broader network handling collections and intimidation.
|
||||||
|
|
||||||
|
Cover stable. Recommend financial surveillance on the regular players, quiet corruption probe on the purported city contacts, and a tailored raid plan that mitigates risk to civilians at the gaming site.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
12
assets/game1/filesystem/var/rapports/064_72-90.txt
Normal file
12
assets/game1/filesystem/var/rapports/064_72-90.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Undercover Report – Case File 72-90
|
||||||
|
Agent: Quinn (cover identity)
|
||||||
|
Date: 08/09 – 08/12
|
||||||
|
|
||||||
|
Subject fronts a small software firm that has attracted high‑risk clients seeking data‑scraping and intrusion services. Agent embedded as a temporary QA tester, which offered proximity to build pipelines and the staging server. On 08/10, the subject approved a late‑night push titled “crawler‑plus” that contained modules for credential stuffing and proxy rotation. The code branch was kept off the main repository and shared via encrypted zip.
|
||||||
|
|
||||||
|
On 08/11, two visitors arrived with no badges and were escorted directly to the conference room. Subject requested the agent run a “sandbox smoke test” while the visitors watched a dashboard of login attempts against third‑party targets. Conversation referenced “clean lists,” “UID harvest,” and “deliverables by Friday.”
|
||||||
|
|
||||||
|
On 08/12, the subject floated a contract expansion involving a custom build for “telecom metadata capture.” When the agent hesitated, the subject advised to “just test the pipeline; leave the contracts to me.” Security posture inside the firm is lax; logs rotate every twenty‑four hours without retention.
|
||||||
|
|
||||||
|
Cover credible. Recommend rapid legal hold to preserve server images, quiet outreach to targeted platforms to harden defenses, and preparation for a coordinated search warrant before the next deliverables window.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
12
assets/game1/filesystem/var/rapports/072_61-05.txt
Normal file
12
assets/game1/filesystem/var/rapports/072_61-05.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Undercover Report – Case File 61-05
|
||||||
|
Agent: Carter (cover identity)
|
||||||
|
Date: 03/14 – 03/17
|
||||||
|
|
||||||
|
Subject operates a livestock auction and feed cooperative used as a cash‑handling hub for a broader laundering scheme. Agent’s cover as a regional feed rep granted access to the back office and the weigh‑ticket printer. On 03/15, the subject directed staff to reprint weights for three lots and staple them over originals, inflating values by roughly thirty percent. Cash payouts were then split into smaller envelopes labeled as “hauler fees.”
|
||||||
|
|
||||||
|
On 03/16, a trailer arrived after closing with no consignor paperwork. Subject ushered the driver to the scales, then recorded a manual weight entry without zeroing the platform. Later, agent noted the same trailer departed with bales stacked higher than safety guidelines, suggesting a swap on‑site.
|
||||||
|
|
||||||
|
On 03/17, the subject met with a banker in the café booth. Heard fragments: “seasonal float,” “parcel deposits,” and “don’t trigger CTRs.” The banker left through the side door, avoiding cameras.
|
||||||
|
|
||||||
|
Cover remains intact. Recommend forensic review of scale logs, unannounced compliance checks tied to animal‑welfare authorities as legitimate cover, and analysis of deposit structuring patterns that match the inflated weigh‑tickets.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
12
assets/game1/filesystem/var/rapports/075_46-77.txt
Normal file
12
assets/game1/filesystem/var/rapports/075_46-77.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Undercover Report – Case File 46-77
|
||||||
|
Agent: Ellis (cover identity)
|
||||||
|
Date: 11/08 – 11/11
|
||||||
|
|
||||||
|
Subject supervises a container‑yard subcontractor with access to high‑value import bays. Agent entered as a crane‑operator trainee assigned to night shifts. On 11/09, subject instructed the crew to reposition a forty‑footer from the manifest queue to an unnumbered slot behind the scrap pile. The container seal had a fresh dab of paint across the latch consistent with a re‑applied counterfeit seal. Yard cameras were left panning, creating reliable blind spots of twenty to thirty seconds.
|
||||||
|
|
||||||
|
On 11/10, the subject met with two visitors at the fence line and exchanged a clipboard without approaching the office. Agent overheard them discuss “blank HS codes” and “holiday backlog.” The conversation implies a counterfeit‑electronics shipment misdeclared to avoid inspection. Later, subject distributed burner keycards for the east pedestrian gate, instructing workers to “switch badges” if stopped.
|
||||||
|
|
||||||
|
On 11/11, a third party attempted to tip the agent to accept overtime in exchange for keeping a particular bay unsupervised. Agent declined while maintaining cover as rule‑bound but inexperienced. The subject appeared satisfied the lane would remain quiet.
|
||||||
|
|
||||||
|
Cover intact. Recommend customs partnership to flag the suspect container by serial, deploy a mobile X‑ray with minimal yard disruption, and audit badge access logs for the east gate.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
12
assets/game1/filesystem/var/rapports/079_89-47.txt
Normal file
12
assets/game1/filesystem/var/rapports/079_89-47.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Undercover Report – Case File 89-47
|
||||||
|
Agent: Dalton (cover identity)
|
||||||
|
Date: 10/05 – 10/08
|
||||||
|
|
||||||
|
Subject leads a motorcycle club that moonlights as a gunrunning and protection outfit. Agent entered as an auto detailer offering cut‑rate work on club vehicles. On 10/06, the subject invited the agent to a back‑lot barbecue where patched members traded cash for small boxes moved from saddlebag to saddlebag. A prospect muttered about “.30 cal uppers” and a “desert range test.”
|
||||||
|
|
||||||
|
On 10/07, the club sergeant‑at‑arms demanded that the agent fix a VIP’s truck immediately, creating a loyalty test. While retrieving tools, the agent observed a crate under a tarp with oil‑paper‑wrapped parts and a stencil for a defunct manufacturer. Numbers were partially filed. The subject later bragged about buyers “east of the pass.”
|
||||||
|
|
||||||
|
On 10/08, a convoy departed in staggered pairs. The subject ordered phones in airplane mode and handed out paper maps with pre‑drawn detours. A follower crashed a bike on loose gravel; the group paused in a blind spot, repositioned a crate, and continued.
|
||||||
|
|
||||||
|
Cover remains plausible. Recommend roadside interdiction coordinated with Highway Patrol, ballistics work on recovered parts, and asset development targeting the prospect who appears chatty under alcohol.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
11
assets/game1/filesystem/var/rapports/083_25-39.txt
Normal file
11
assets/game1/filesystem/var/rapports/083_25-39.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Undercover Report – Case File 25-39
|
||||||
|
Agent: Doyle (cover identity)
|
||||||
|
Date: 04/03 – 04/06
|
||||||
|
|
||||||
|
Subject operates a small river ferry and maintenance dock that appear to facilitate contraband transfers between barges at off‑schedule hours. Agent’s cover as an out‑of‑work boat mechanic provided routine access to bilge areas and tool lockers. On 04/04, subject instructed the crew to keep the north slip unlit after midnight and to disable the dock camera claiming a blown fuse. The camera was found unplugged rather than damaged. Two crates moved from a barge to a skiff without paperwork.
|
||||||
|
|
||||||
|
On 04/05, agent overheard the subject arguing with a radio contact over “ore purity” and “assay timing.” The phrasing suggests smuggling of high‑value metals rather than typical street narcotics. Crew members wore gloves and avoided scraping the crates’ corners, reinforcing the inference of dense contents. A handheld Geiger counter case was observed in the tool cage, but no reading event was witnessed.
|
||||||
|
|
||||||
|
On 04/06, the subject requested help fabricating a false engine‑repair ticket to explain downtime for Barge 12. Ticket was signed under a false corporate name and backdated. Subject appeared calm after the forgery, then paid crew in cash envelopes with inconsistent denominations.
|
||||||
|
|
||||||
|
Cover intact. Recommend discreet sampling of riverbed residue near the north slip, cross‑checking barge manifests for Barge 12, and coordination with environmental regulators to justify covert inspections without alerting the subject.
|
||||||
10
assets/game1/filesystem/var/rapports/087_102-45.txt
Normal file
10
assets/game1/filesystem/var/rapports/087_102-45.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Undercover Report – Case File 102-45
|
||||||
|
Agent: Sanders (cover identity)
|
||||||
|
Date: 08/19 – 08/23
|
||||||
|
|
||||||
|
Subject manages an upscale restaurant suspected of laundering illicit gambling proceeds. Agent embedded as a sommelier, granting access to private dining rooms and basement storage. On 08/20, the subject hosted three suited men; the room was sealed and guarded. While pouring, the agent heard references to “clearing accounts” and “west‑coast expansion.” The subject kept a handwritten ledger with columns that do not reconcile with the point‑of‑sale exports.
|
||||||
|
|
||||||
|
On 08/22, a delivery van bypassed normal suppliers and offloaded crates into the walk‑in freezer with no perishables labeling. Handlers displayed tactical posture inconsistent with food service. On 08/23, the subject warned staff about inspections and emphasized silence, revealing controlled anxiety.
|
||||||
|
|
||||||
|
Cover remains secure; agent perceived as competent and unthreatening. Recommend parallel financial audit, supplier‑chain verification, and a timed inspection aligned with the off‑cycle delivery pattern.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
12
assets/game1/filesystem/var/rapports/091_81-33.txt
Normal file
12
assets/game1/filesystem/var/rapports/091_81-33.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Undercover Report – Case File 81-33
|
||||||
|
Agent: Raines (cover identity)
|
||||||
|
Date: 01/18 – 01/21
|
||||||
|
|
||||||
|
Subject manages a boutique hotel that periodically closes floors for “private events.” Agent’s cover as night‑shift concierge grants exposure to guest registries and key‑encoder logs. On 01/19, the subject blocked out Level 5 under a shell company and delivered sealed envelopes to three suites. Noise and foot traffic suggested an invitation‑only auction. Staff were instructed to use service stairs only and disable the lobby feed for two hours citing maintenance.
|
||||||
|
|
||||||
|
On 01/20, two SUVs with obscured plates arrived at the loading bay. Items resembling art crates and a locked pelican case were transferred to Suite 512. A known broker briefly appeared in the hallway, then vanished into the service lift. The minibar inventory showed no consumption despite the headcount—common during short, transactional gatherings.
|
||||||
|
|
||||||
|
On 01/21, the subject praised the agent for “discretion” and hinted at a permanent role. Later, housekeeping turned over a discarded bidder paddlestick with handwritten totals. Figures exceeded declared room revenue by a wide margin.
|
||||||
|
|
||||||
|
Cover holds. Recommend targeted warrants for the shell company, a parallel probe into the broker, and a controlled interruption on the next “private” floor closure to preserve evidence while minimizing guest disruption.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
10
assets/game1/filesystem/var/rapports/093_138-24.txt
Normal file
10
assets/game1/filesystem/var/rapports/093_138-24.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Undercover Report – Case File 138-24
|
||||||
|
Agent: Bennett (cover identity)
|
||||||
|
Date: 01/12 – 01/15
|
||||||
|
|
||||||
|
Subject manages a modest construction company that appears to use job sites for concealment and cash skimming. Agent hired as temporary labor. On 01/13, a hidden compartment beneath scaffolding yielded two heavy duffels that the subject moved to a pickup without logging materials. The foreman kept crew distant with a noise‑control pretext.
|
||||||
|
|
||||||
|
On 01/14, the subject argued with a partner about “federal contracts” and “kickbacks,” implying bid‑rigging and public‑corruption exposure. On 01/15, the subject praised the agent’s work and hinted at “bigger projects,” a sign of attempted grooming for riskier tasks.
|
||||||
|
|
||||||
|
Cover maintained. Recommend forensic review of procurement records, site searches keyed to concealed compartments, and interviews with subcontractors likely aware of the skimming practice.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
10
assets/game1/filesystem/var/rapports/094_110-19.txt
Normal file
10
assets/game1/filesystem/var/rapports/094_110-19.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Undercover Report – Case File 110-19
|
||||||
|
Agent: Mitchell (cover identity)
|
||||||
|
Date: 09/07 – 09/10
|
||||||
|
|
||||||
|
Subject runs a repair garage that doubles as a weapons workshop. Agent embedded as a part‑time apprentice. On 09/08, the subject modified rifles at a back bench outfitted with jigs, thread cutters, and solvent traps. Multiple unserialized receivers were visible. The subject bragged about clients who “pay for silence.”
|
||||||
|
|
||||||
|
On 09/09, a phone call arranged delivery of “three crates” to a desert rendezvous, specifying urgency and “military‑grade parts.” On 09/10, the subject tested the agent with a drop of a box labeled scrap to an abandoned rail yard. The weight and rattle suggested mixed metal components. Delivery completed under surveillance without opening to preserve cover.
|
||||||
|
|
||||||
|
Cover intact though scrutiny is increasing. Recommend immediate tracing of the rail‑yard contact, controlled intercept of the desert transfer, and NIBIN checks on recovered parts.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
11
assets/game1/filesystem/var/rapports/095_07-14.txt
Normal file
11
assets/game1/filesystem/var/rapports/095_07-14.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Undercover Report – Case File 07-14
|
||||||
|
Agent: Mason (cover identity)
|
||||||
|
Date: 06/12 – 06/15
|
||||||
|
|
||||||
|
Subject continues to operate a cash‑heavy nightclub believed to be central to mid‑level narcotics distribution in the northwest sector. Cover identity as part‑time bartender remains effective and provides nightly access to storeroom traffic and the restricted back hallway behind the stage. On 06/13, the subject supervised delivery of three sealed cartons labeled as beverage syrups. The weight and the handlers’ posture suggested dense contents inconsistent with that label. Subject instructed all staff to avoid contact and to log the cartons as vendor overstock without serials.
|
||||||
|
|
||||||
|
On 06/14, the subject held a closed‑door conversation with a courier known locally as “Rico.” Portions overheard included references to a “Miami truck” and “east‑coast push.” Tone implied urgency and reliance on corrupt support, described as “our friend at permits.” Identity of the alleged official remains unknown. Later that evening, a floor manager tested staff loyalty by asking casual questions about police patrol patterns near 12th Street.
|
||||||
|
|
||||||
|
On 06/15, subject gave the agent an envelope for a routine off‑site drop. Envelope was briefly photographed under pretext of retrieving car keys; contents included a short ledger with coded initials, tally marks, and four phone numbers written in alternating ink colors. Envelope was delivered intact to avoid suspicion. Subject’s post‑drop demeanor appeared relaxed, suggesting the transaction cleared immediate obligations.
|
||||||
|
|
||||||
|
Cover remains intact, with agent perceived as reliable but peripheral. Recommend discreet financial analysis of the ledger numbers, targeted canvass for the unknown permits contact, and coordination with port‑of‑entry teams to watch for south‑to‑north freight aligning with subject’s timetable.
|
||||||
12
assets/game1/filesystem/var/rapports/098_92-14.txt
Normal file
12
assets/game1/filesystem/var/rapports/098_92-14.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Undercover Report – Case File 92-14
|
||||||
|
Agent: Langston (cover identity)
|
||||||
|
Date: 05/10 – 05/14
|
||||||
|
|
||||||
|
Subject continues to expand operations through a network of import/export fronts in the industrial docklands. Cover identity as an independent freight consultant has provided access to routing meetings and late‑night inspections. On 05/11, the subject oversaw containers labeled “agricultural machinery” with weights above manifest and brand‑new locks. Handlers were armed and avoided eye contact. No safe chance to open containers without exposure.
|
||||||
|
|
||||||
|
On 05/12, the subject introduced an associate, “Gallo,” who quizzed the agent on Gulf‑port customs routines and inspection frequencies. Questions implied an imminent push of high‑value contraband through maritime lanes with compromised timing windows. The crew canceled two runs after learning of a regional seizure, and the subject complained about “partners overseas losing patience.”
|
||||||
|
|
||||||
|
On 05/14, a briefcase exchange at a dockside café lasted under two minutes; the subject accepted the case and passed it to Gallo without inspection. Both appeared tense but determined.
|
||||||
|
|
||||||
|
Cover remains stable yet fragile. Recommend expanded surveillance at the targeted warehouses, customs liaisons to flag the specific manifest anomalies noted, and a rapid extraction plan if the crew pivots to violent vetting.
|
||||||
|
Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible. Cover remains intact. Recommend continued surveillance and coordinated warrants when feasible.
|
||||||
20
assets/game1/filesystem/var/rapports/index.txt
Normal file
20
assets/game1/filesystem/var/rapports/index.txt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
095_07-14.txt – 07-14 – Mason
|
||||||
|
007_19-52.txt – 19-52 – Harper
|
||||||
|
083_25-39.txt – 25-39 – Doyle
|
||||||
|
019_31-11.txt – 31-11 – Vega
|
||||||
|
075_46-77.txt – 46-77 – Ellis
|
||||||
|
031_53-28.txt – 53-28 – Navarro
|
||||||
|
072_61-05.txt – 61-05 – Carter
|
||||||
|
064_72-90.txt – 72-90 – Quinn
|
||||||
|
091_81-33.txt – 81-33 – Raines
|
||||||
|
079_89-47.txt – 89-47 – Dalton
|
||||||
|
098_92-14.txt – 92-14 – Langston
|
||||||
|
012_94-31.txt – 94-31 – Donovan
|
||||||
|
016_98-07.txt – 98-07 – Fletcher
|
||||||
|
087_102-45.txt – 102-45 – Sanders
|
||||||
|
094_110-19.txt – 110-19 – Mitchell
|
||||||
|
063_117-56.txt – 117-56 – O’Connor
|
||||||
|
017_123-88.txt – 123-88 – Holloway
|
||||||
|
011_130-62.txt – 130-62 – Lennox
|
||||||
|
093_138-24.txt – 138-24 – Bennett
|
||||||
|
001_145-93.txt – 145-93 – Carver
|
||||||
@@ -1,17 +1,93 @@
|
|||||||
/* Styles specific to Game1 */
|
/* Styles specific to Game1 */
|
||||||
|
|
||||||
/* page-level indicator to confirm CSS is loaded */
|
/* page-level indicator to confirm CSS is loaded */
|
||||||
body.game1-page {
|
/* Custom scrollbar for WebKit browsers */
|
||||||
/* subtle background tint so you can visually confirm on /game */
|
html::-webkit-scrollbar,
|
||||||
background-color: #f9fbff;
|
body::-webkit-scrollbar {
|
||||||
|
width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* example component style */
|
html::-webkit-scrollbar-track,
|
||||||
.game1-banner {
|
body::-webkit-scrollbar-track {
|
||||||
padding: 1rem 1.25rem;
|
background: #000;
|
||||||
border: 1px solid #cfe2ff;
|
}
|
||||||
background: #e9f2ff;
|
|
||||||
color: #0b5ed7;
|
html::-webkit-scrollbar-thumb,
|
||||||
border-radius: 8px;
|
body::-webkit-scrollbar-thumb {
|
||||||
margin: 1rem 0;
|
background: #F00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Standard properties for Firefox */
|
||||||
|
body {
|
||||||
|
background-color: #000;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: monospace;
|
||||||
|
margin: 0;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #F00 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#game-timer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #000;
|
||||||
|
color: #F00;
|
||||||
|
font-size: 28px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#message-container {
|
||||||
|
padding: 20px;
|
||||||
|
padding-top: 80px; /* Space for fixed timer */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
min-height: calc(100vh - 100px); /* Fill most of the viewport initially */
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.message {
|
||||||
|
color: #C0C0C0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#input {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input#input-message {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background: #111;
|
||||||
|
border: 1px solid #A00000;
|
||||||
|
color: #C0C0C0;
|
||||||
|
font-size: 18px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-red::after {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100px;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(to top, rgba(255, 0, 0, 0.8), transparent);
|
||||||
|
animation: flash-red-anim 0.1s linear forwards;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flash-red-anim {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
services:
|
|
||||||
###> symfony/mercure-bundle ###
|
|
||||||
mercure:
|
|
||||||
ports:
|
|
||||||
- "80"
|
|
||||||
###< symfony/mercure-bundle ###
|
|
||||||
31
compose.yaml
31
compose.yaml
@@ -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 ###
|
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"doctrine/doctrine-bundle": "^2.16",
|
"doctrine/doctrine-bundle": "^2.16",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.4",
|
"doctrine/doctrine-migrations-bundle": "^3.4",
|
||||||
"doctrine/orm": "^3.5",
|
"doctrine/orm": "^3.5",
|
||||||
|
"karser/karser-recaptcha3-bundle": "^0.3.0",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
"phpstan/phpdoc-parser": "^2.3",
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
"symfony/asset": "7.3.*",
|
"symfony/asset": "7.3.*",
|
||||||
|
|||||||
84
composer.lock
generated
84
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "8e2419832c0841e325a5b748bde61a48",
|
"content-hash": "22d46e70bd8246939c294d28ecfff13d",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "composer/semver",
|
"name": "composer/semver",
|
||||||
@@ -1271,6 +1271,88 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-03-06T22:45:56+00:00"
|
"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",
|
"name": "lcobucci/jwt",
|
||||||
"version": "5.6.0",
|
"version": "5.6.0",
|
||||||
|
|||||||
@@ -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,6 +1,14 @@
|
|||||||
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)
|
||||||
|
|||||||
@@ -8,7 +8,21 @@ 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%'
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
5
config/packages/karser_recaptcha3.yaml
Normal file
5
config/packages/karser_recaptcha3.yaml
Normal file
@@ -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,5 @@ 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:
|
||||||
|
value: '%env(MERCURE_PUBLISHER_JWT_TOKEN)%'
|
||||||
|
|||||||
@@ -26,4 +26,4 @@ framework:
|
|||||||
Symfony\Component\Notifier\Message\SmsMessage: async
|
Symfony\Component\Notifier\Message\SmsMessage: async
|
||||||
|
|
||||||
# Route your messages to the transports
|
# Route your messages to the transports
|
||||||
# 'App\Message\YourMessage': async
|
'App\Tech\Message\ProcessTaskMessage': async
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -30,6 +30,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 }
|
||||||
|
|
||||||
|
|||||||
2
config/packages/test/karser_recaptcha3.yaml
Normal file
2
config/packages/test/karser_recaptcha3.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
karser_recaptcha3:
|
||||||
|
enabled: false
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ 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.
|
||||||
|
|
||||||
|
|||||||
56
docker/.env
Normal file
56
docker/.env
Normal 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
29
docker/.env.dist
Normal 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
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
|
|
||||||
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:
|
||||||
@@ -23,4 +22,17 @@ services:
|
|||||||
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 ###
|
||||||
|
|||||||
@@ -1,21 +1,69 @@
|
|||||||
|
|
||||||
version: '3.7'
|
version: '3.7'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
php:
|
php:
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: php/Dockerfile
|
dockerfile: docker/php/Dockerfile
|
||||||
|
args:
|
||||||
|
USER_ID: ${USER_ID}
|
||||||
|
GROUP_ID: ${GROUP_ID}
|
||||||
container_name: escapepage-php
|
container_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
|
||||||
|
|
||||||
|
php-worker:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/php/Dockerfile
|
||||||
|
args:
|
||||||
|
USER_ID: ${USER_ID}
|
||||||
|
GROUP_ID: ${GROUP_ID}
|
||||||
|
container_name: escapepage-php-worker
|
||||||
|
volumes:
|
||||||
|
- ../:/var/www/html:delegated
|
||||||
|
- /etc/hosts:/etc/hosts:ro
|
||||||
|
environment:
|
||||||
|
APP_ENV: ${APP_ENV}
|
||||||
|
SITE_BASE_URL: ${SITE_BASE_URL}
|
||||||
|
MAILER_DSN: ${MAILER_DSN}
|
||||||
|
MAILER_FROM: ${MAILER_FROM}
|
||||||
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
|
MERCURE_URL: ${MERCURE_URL}
|
||||||
|
MERCURE_PUBLIC_URL: ${MERCURE_PUBLIC_URL}
|
||||||
|
MERCURE_JWT_SECRET: ${MERCURE_JWT_SECRET}
|
||||||
|
MERCURE_CORS_ALLOWED_ORIGINS: ${MERCURE_CORS_ALLOWED_ORIGINS}
|
||||||
|
MERCURE_TOPIC_BASE: ${MERCURE_TOPIC_BASE}
|
||||||
|
RECAPTCHA3_KEY: ${RECAPTCHA3_KEY}
|
||||||
|
RECAPTCHA3_SECRET: ${RECAPTCHA3_SECRET}
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
- mercure
|
||||||
|
command: ["php", "bin/console", "messenger:consume", "async", "-vv"]
|
||||||
|
# networks:
|
||||||
|
# backend:
|
||||||
|
# ipv4_address: 172.23.0.11
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
@@ -23,13 +71,17 @@ services:
|
|||||||
container_name: escapepage-nginx
|
container_name: escapepage-nginx
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
|
- "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
|
||||||
networks:
|
# networks:
|
||||||
- backend
|
# backend:
|
||||||
|
# ipv4_address: 172.23.0.12
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
mailer:
|
mailer:
|
||||||
@@ -37,60 +89,75 @@ services:
|
|||||||
container_name: escapepage-mailer
|
container_name: escapepage-mailer
|
||||||
ports:
|
ports:
|
||||||
- "8025:8025"
|
- "8025:8025"
|
||||||
networks:
|
volumes:
|
||||||
- backend
|
- /etc/hosts:/etc/hosts:ro
|
||||||
|
# networks:
|
||||||
|
# backend:
|
||||||
|
# ipv4_address: 172.23.0.13
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
mercure:
|
mercure:
|
||||||
image: dunglas/mercure:v0.21
|
image: dunglas/mercure:v0.21
|
||||||
container_name: escapepage-mercure
|
container_name: escapepage-mercure
|
||||||
environment:
|
environment:
|
||||||
SERVER_NAME: ":80"
|
SERVER_NAME: "https://:443"
|
||||||
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"
|
- "8090:443"
|
||||||
networks:
|
volumes:
|
||||||
- backend
|
- /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
|
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: 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"
|
- "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:
|
# backend:
|
||||||
driver: bridge
|
# name: escapepage_network
|
||||||
|
# driver: bridge
|
||||||
|
# ipam:
|
||||||
|
# config:
|
||||||
|
# - subnet: 172.23.0.0/16
|
||||||
|
# gateway: 172.23.0.1
|
||||||
|
# attachable: true
|
||||||
|
|||||||
5
docker/mysql/init/allow_remote.sql
Normal file
5
docker/mysql/init/allow_remote.sql
Normal 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;
|
||||||
@@ -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 {
|
||||||
|
|||||||
22
docker/nginx/ssl/server.crt
Normal file
22
docker/nginx/ssl/server.crt
Normal 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-----
|
||||||
28
docker/nginx/ssl/server.key
Normal file
28
docker/nginx/ssl/server.key
Normal 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-----
|
||||||
@@ -1,11 +1,37 @@
|
|||||||
FROM php:8.5.1-fpm-alpine3.23
|
FROM php:8.3-fpm-alpine
|
||||||
|
|
||||||
# Install system deps
|
# Install system deps
|
||||||
RUN apk add --no-cache bash git icu-dev libzip-dev oniguruma-dev
|
RUN apk add --no-cache \
|
||||||
|
bash \
|
||||||
|
git \
|
||||||
|
icu-dev \
|
||||||
|
libzip-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
oniguruma-dev \
|
||||||
|
g++ \
|
||||||
|
make \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
shadow
|
||||||
|
|
||||||
|
# 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
|
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 \
|
||||||
@@ -13,9 +39,25 @@ ENV COMPOSER_ALLOW_SUPERUSER=1 \
|
|||||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
# Configure PHP
|
# Configure PHP
|
||||||
COPY php.ini $PHP_INI_DIR/conf.d/zz-custom.ini
|
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
|
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
|
# Default command
|
||||||
CMD ["php-fpm"]
|
CMD ["php-fpm"]
|
||||||
|
|||||||
@@ -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/errorlog_php.log
|
||||||
|
session.gc_maxlifetime=1440
|
||||||
|
session.cookie_lifetime=0
|
||||||
|
|||||||
29
docker/restart.sh
Normal file
29
docker/restart.sh
Normal 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
|
||||||
@@ -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 --env-file ../.env "$@"); }
|
||||||
|
|
||||||
REBUILD=1
|
REBUILD=1
|
||||||
RECREATE=0
|
RECREATE=0
|
||||||
@@ -61,17 +61,20 @@ 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 "$@"; }
|
||||||
|
|
||||||
# Wait for database to be healthy (mariadb)
|
# Wait for database to be healthy (mariadb/mysql)
|
||||||
printf "Waiting for database to be healthy..."
|
printf "Waiting for database to be healthy..."
|
||||||
# Use docker inspect health status
|
# Use docker inspect health status
|
||||||
DB_HEALTH=""
|
DB_HEALTH=""
|
||||||
for i in {1..60}; do
|
for i in {1..60}; do
|
||||||
DB_HEALTH=$(docker inspect -f '{{.State.Health.Status}}' "$(docker ps --filter name=_database_ --format '{{.ID}}' | head -n1)" 2>/dev/null || true)
|
DB_ID=$(dc ps -q database 2>/dev/null || true)
|
||||||
|
if [ -n "$DB_ID" ]; then
|
||||||
|
DB_HEALTH=$(docker inspect -f '{{.State.Health.Status}}' "$DB_ID" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
if [ "$DB_HEALTH" = "healthy" ]; then
|
if [ "$DB_HEALTH" = "healthy" ]; then
|
||||||
echo " OK"
|
echo " OK"
|
||||||
break
|
break
|
||||||
@@ -79,7 +82,7 @@ for i in {1..60}; do
|
|||||||
printf "."
|
printf "."
|
||||||
sleep 2
|
sleep 2
|
||||||
if [ "$i" -eq 60 ]; then
|
if [ "$i" -eq 60 ]; then
|
||||||
echo "\nWarning: database health check not healthy yet, continuing anyway."
|
echo -e "\nWarning: database health check not healthy yet, continuing anyway."
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -100,11 +103,31 @@ if grep -q '^APP_SECRET=$' "$ROOT_DIR/.env" 2>/dev/null; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Prepare DB
|
# Prepare DB
|
||||||
pexec php bin/console doctrine:database:create --if-not-exists || true
|
echo "Creating database if it doesn't exist..."
|
||||||
pexec php bin/console doctrine:migrations:migrate -n || true
|
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..."
|
||||||
|
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)
|
||||||
pexec php bin/console importmap:install || true
|
if [ -f "$ROOT_DIR/importmap.php" ]; then
|
||||||
|
pexec php bin/console importmap:install || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build assets if using Webpack Encore
|
||||||
|
if [ -f "$ROOT_DIR/package.json" ]; then
|
||||||
|
echo "Installing npm dependencies..."
|
||||||
|
pexec npm install
|
||||||
|
echo "Building assets..."
|
||||||
|
pexec npm run build
|
||||||
|
fi
|
||||||
|
|
||||||
APP_URL=http://localhost:8080
|
APP_URL=http://localhost:8080
|
||||||
MAILPIT_URL=http://localhost:8025
|
MAILPIT_URL=http://localhost:8025
|
||||||
@@ -117,10 +140,12 @@ Open the app: $APP_URL
|
|||||||
Mailpit (dev): $MAILPIT_URL
|
Mailpit (dev): $MAILPIT_URL
|
||||||
|
|
||||||
Common commands:
|
Common commands:
|
||||||
(cd docker && $DOCKER_COMPOSE logs -f nginx)
|
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE logs -f nginx)
|
||||||
(cd docker && $DOCKER_COMPOSE logs -f php)
|
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE logs -f php)
|
||||||
(cd docker && $DOCKER_COMPOSE exec php bash)
|
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE logs -f php-worker)
|
||||||
(cd docker && $DOCKER_COMPOSE down)
|
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE exec php bash)
|
||||||
|
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE exec php npm run watch)
|
||||||
|
(cd "$DOCKER_DIR" && $DOCKER_COMPOSE down)
|
||||||
|
|
||||||
You can re-run this script any time. Use --no-build to skip rebuilding images.
|
You can re-run this script any time. Use --no-build to skip rebuilding images.
|
||||||
EOT
|
EOT
|
||||||
|
|||||||
41
migrations/Version20260105113139.php
Normal file
41
migrations/Version20260105113139.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?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 Version20260105113139 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE game (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, number_of_players INT NOT NULL, status VARCHAR(20) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('CREATE TABLE player (id INT AUTO_INCREMENT NOT NULL, session_id INT NOT NULL, user_id INT NOT NULL, screen INT NOT NULL, INDEX IDX_98197A65613FECDF (session_id), INDEX IDX_98197A65A76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('CREATE TABLE session (id INT AUTO_INCREMENT NOT NULL, game_id INT NOT NULL, status VARCHAR(20) NOT NULL, timer INT NOT NULL, created DATETIME NOT NULL, INDEX IDX_D044D5D4E48FD905 (game_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('ALTER TABLE player ADD CONSTRAINT FK_98197A65613FECDF FOREIGN KEY (session_id) REFERENCES session (id)');
|
||||||
|
$this->addSql('ALTER TABLE player ADD CONSTRAINT FK_98197A65A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
|
||||||
|
$this->addSql('ALTER TABLE session ADD CONSTRAINT FK_D044D5D4E48FD905 FOREIGN KEY (game_id) REFERENCES game (id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE player DROP FOREIGN KEY FK_98197A65613FECDF');
|
||||||
|
$this->addSql('ALTER TABLE player DROP FOREIGN KEY FK_98197A65A76ED395');
|
||||||
|
$this->addSql('ALTER TABLE session DROP FOREIGN KEY FK_D044D5D4E48FD905');
|
||||||
|
$this->addSql('DROP TABLE game');
|
||||||
|
$this->addSql('DROP TABLE player');
|
||||||
|
$this->addSql('DROP TABLE session');
|
||||||
|
}
|
||||||
|
}
|
||||||
31
migrations/Version20260105121159.php
Normal file
31
migrations/Version20260105121159.php
Normal 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 Version20260105121159 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE player ADD level JSON 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 DROP level');
|
||||||
|
}
|
||||||
|
}
|
||||||
39
migrations/Version20260105155949.php
Normal file
39
migrations/Version20260105155949.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?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 Version20260105155949 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE game_setting (id INT AUTO_INCREMENT NOT NULL, game_id INT NOT NULL, name VARCHAR(255) NOT NULL, value LONGTEXT DEFAULT NULL, INDEX IDX_AB6C7B7E48FD905 (game_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('CREATE TABLE session_setting (id INT AUTO_INCREMENT NOT NULL, session_id INT NOT NULL, player_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, value LONGTEXT DEFAULT NULL, INDEX IDX_8DAC3AC2613FECDF (session_id), INDEX IDX_8DAC3AC299E6F5DF (player_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('ALTER TABLE game_setting ADD CONSTRAINT FK_AB6C7B7E48FD905 FOREIGN KEY (game_id) REFERENCES game (id)');
|
||||||
|
$this->addSql('ALTER TABLE session_setting ADD CONSTRAINT FK_8DAC3AC2613FECDF FOREIGN KEY (session_id) REFERENCES session (id)');
|
||||||
|
$this->addSql('ALTER TABLE session_setting ADD CONSTRAINT FK_8DAC3AC299E6F5DF FOREIGN KEY (player_id) REFERENCES player (id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE game_setting DROP FOREIGN KEY FK_AB6C7B7E48FD905');
|
||||||
|
$this->addSql('ALTER TABLE session_setting DROP FOREIGN KEY FK_8DAC3AC2613FECDF');
|
||||||
|
$this->addSql('ALTER TABLE session_setting DROP FOREIGN KEY FK_8DAC3AC299E6F5DF');
|
||||||
|
$this->addSql('DROP TABLE game_setting');
|
||||||
|
$this->addSql('DROP TABLE session_setting');
|
||||||
|
}
|
||||||
|
}
|
||||||
31
migrations/Version20260105160603.php
Normal file
31
migrations/Version20260105160603.php
Normal 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 Version20260105160603 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE player DROP level');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE player ADD level JSON DEFAULT NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
31
migrations/Version20260117143000.php
Normal file
31
migrations/Version20260117143000.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/audio/message.mp3
Normal file
BIN
public/audio/message.mp3
Normal file
Binary file not shown.
@@ -1,9 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Kernel;
|
use App\Kernel;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||||
|
|
||||||
return function (array $context) {
|
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']);
|
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||||
};
|
};
|
||||||
|
|||||||
63
src/Game/Controller/GameAdminController.php
Normal file
63
src/Game/Controller/GameAdminController.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Game\Controller;
|
||||||
|
|
||||||
|
use App\Game\Entity\Session;
|
||||||
|
use App\Game\Repository\SessionRepository;
|
||||||
|
use App\Tech\Repository\UserRepository;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
|
||||||
|
#[Route('/admin')]
|
||||||
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
|
final class GameAdminController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[Autowire('%kernel.project_dir%')]
|
||||||
|
private string $projectDir
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('', name: 'game_admin_dashboard', methods: ['GET'])]
|
||||||
|
public function index(
|
||||||
|
UserRepository $userRepository,
|
||||||
|
SessionRepository $sessionRepository
|
||||||
|
): Response {
|
||||||
|
$players = $userRepository->findByRole('ROLE_PLAYER');
|
||||||
|
$sessions = $sessionRepository->findAll();
|
||||||
|
|
||||||
|
return $this->render('game/admin/index.html.twig', [
|
||||||
|
'players' => $players,
|
||||||
|
'sessions' => $sessions,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/session/{session}', name: 'game_admin_view_session', methods: ['GET'])]
|
||||||
|
public function viewSession(Session $session): Response
|
||||||
|
{
|
||||||
|
$playersLogs = [];
|
||||||
|
foreach ($session->getPlayers() as $player) {
|
||||||
|
$username = $player->getUser()->getUsername();
|
||||||
|
$logFile = $this->projectDir . '/var/log/sessions/' . $session->getId() . '/' . $username . '.txt';
|
||||||
|
|
||||||
|
$logs = '';
|
||||||
|
if (file_exists($logFile)) {
|
||||||
|
$logs = file_get_contents($logFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
$playersLogs[] = [
|
||||||
|
'username' => $username,
|
||||||
|
'logs' => $logs,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('game/admin/session.html.twig', [
|
||||||
|
'session' => $session,
|
||||||
|
'playersLogs' => $playersLogs,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Game\Controller;
|
namespace App\Game\Controller;
|
||||||
|
|
||||||
|
use App\Game\Entity\Session;
|
||||||
|
use App\Game\Enum\SessionStatus;
|
||||||
use App\Game\Service\GameResponseService;
|
use App\Game\Service\GameResponseService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -15,7 +18,9 @@ final class GameApiController extends AbstractController
|
|||||||
{
|
{
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected GameResponseService $gameResponseService) {
|
protected GameResponseService $gameResponseService,
|
||||||
|
private EntityManagerInterface $entityManager
|
||||||
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +34,30 @@ final class GameApiController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/check-finished/{session}', name: 'check_finished', methods: ['POST'])]
|
||||||
|
public function checkFinished(Session $session): JsonResponse
|
||||||
|
{
|
||||||
|
$now = (new \DateTime())->getTimestamp();
|
||||||
|
$isFinished = false;
|
||||||
|
|
||||||
|
if ($session->getStatus() === SessionStatus::PLAYING) {
|
||||||
|
if ($session->getTimer() !== null && $now >= $session->getTimer()) {
|
||||||
|
$session->setStatus(SessionStatus::LOST);
|
||||||
|
$this->entityManager->persist($session);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$isFinished = true;
|
||||||
|
}
|
||||||
|
} elseif ($session->getStatus() === SessionStatus::LOST || $session->getStatus() === SessionStatus::WON) {
|
||||||
|
$isFinished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'ok' => true,
|
||||||
|
'finished' => $isFinished,
|
||||||
|
'status' => $session->getStatus()->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
#[Route('/message', name: 'message', methods: ['POST'])]
|
#[Route('/message', name: 'message', methods: ['POST'])]
|
||||||
public function message(Request $request): JsonResponse
|
public function message(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,17 +3,229 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Game\Controller;
|
namespace App\Game\Controller;
|
||||||
|
|
||||||
|
use App\Game\Entity\Player;
|
||||||
|
use App\Game\Entity\Session;
|
||||||
|
use App\Game\Entity\SessionSetting;
|
||||||
|
use App\Game\Enum\SessionSettingType;
|
||||||
|
use App\Game\Enum\SessionStatus;
|
||||||
|
use App\Game\Repository\GameRepository;
|
||||||
|
use App\Game\Repository\PlayerRepository;
|
||||||
|
use App\Game\Repository\SessionRepository;
|
||||||
|
use App\Game\Service\GameDashboardService;
|
||||||
|
use App\Tech\Entity\User;
|
||||||
|
use App\Game\Service\PlayerService;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
use Symfony\Component\ExpressionLanguage\Expression;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
|
||||||
final class GameController extends AbstractController
|
final class GameController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route(path: '', name: 'game')]
|
public function __construct(
|
||||||
public function index(): Response
|
#[Autowire('%env(MERCURE_PUBLIC_URL)%')]
|
||||||
{
|
private string $mercurePublicUrl,
|
||||||
return $this->render('game/index.html.twig', [
|
private \Doctrine\ORM\EntityManagerInterface $entityManager
|
||||||
'user_id' => $this->getUser()?->getId(),
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '', name: 'game_dashboard', methods: ['GET', 'POST'])]
|
||||||
|
#[IsGranted(new Expression("is_granted('ROLE_PLAYER') or is_granted('ROLE_ADMIN')"))]
|
||||||
|
public function dashboard(
|
||||||
|
Request $request,
|
||||||
|
GameRepository $gameRepository,
|
||||||
|
SessionRepository $sessionRepository,
|
||||||
|
GameDashboardService $dashboardService,
|
||||||
|
Security $security
|
||||||
|
): Response {
|
||||||
|
$user = $security->getUser();
|
||||||
|
$isAdmin = $this->isGranted('ROLE_ADMIN');
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
if ($request->request->has('create_session')) {
|
||||||
|
$gameId = $request->request->get('game_id');
|
||||||
|
$game = $gameRepository->find($gameId);
|
||||||
|
|
||||||
|
if ($game) {
|
||||||
|
if ($dashboardService->createSession($game, $user, $isAdmin)) {
|
||||||
|
$this->addFlash('success', 'New session created!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($request->request->has('join_session')) {
|
||||||
|
$inviteCode = $request->request->get('invite_code');
|
||||||
|
if ($dashboardService->joinSession($inviteCode, $user)) {
|
||||||
|
$this->addFlash('success', 'Joined session successfully!');
|
||||||
|
} else {
|
||||||
|
$this->addFlash('error', 'Invalid invite code or session full.');
|
||||||
|
}
|
||||||
|
} elseif ($request->request->has('create_invite')) {
|
||||||
|
$sessionId = $request->request->get('session_id');
|
||||||
|
$session = $sessionRepository->find($sessionId);
|
||||||
|
|
||||||
|
if (!$session) {
|
||||||
|
$this->addFlash('error', 'Session not found.');
|
||||||
|
return $this->redirectToRoute('game_dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
$inviteCode = $dashboardService->generateInviteCode($session, $user, $isAdmin);
|
||||||
|
if ($inviteCode) {
|
||||||
|
$this->addFlash('success', 'Invite link created: ' . $inviteCode);
|
||||||
|
}
|
||||||
|
} elseif ($request->request->has('leave_session')) {
|
||||||
|
$sessionId = $request->request->get('session_id');
|
||||||
|
$session = $sessionRepository->find($sessionId);
|
||||||
|
|
||||||
|
if ($session) {
|
||||||
|
if ($dashboardService->leaveSession($session, $user)) {
|
||||||
|
$this->addFlash('success', 'Left session successfully.');
|
||||||
|
} else {
|
||||||
|
$this->addFlash('error', 'Could not leave session (game might have started).');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($request->request->has('start_session')) {
|
||||||
|
$sessionId = $request->request->get('session_id');
|
||||||
|
$session = $sessionRepository->find($sessionId);
|
||||||
|
|
||||||
|
if ($session) {
|
||||||
|
if ($dashboardService->startSession($session)) {
|
||||||
|
$this->addFlash('success', 'Session started! Screens have been assigned.');
|
||||||
|
} else {
|
||||||
|
$this->addFlash('error', 'Could not start session. Make sure all players have joined.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('game_dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('game/dashboard.html.twig', [
|
||||||
|
'sessions' => $dashboardService->getSessionsForUser($user),
|
||||||
|
'availableGames' => $dashboardService->getAvailableGames($isAdmin),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/{session}', name: 'game', methods: ['GET', 'POST'])]
|
||||||
|
#[IsGranted(new Expression("is_granted('ROLE_PLAYER') or is_granted('ROLE_ADMIN')"))]
|
||||||
|
#[IsGranted('SESSION_VIEW', subject: 'session')]
|
||||||
|
public function index(
|
||||||
|
Session $session,
|
||||||
|
Request $request,
|
||||||
|
Security $security,
|
||||||
|
PlayerRepository $playerRepository,
|
||||||
|
GameDashboardService $dashboardService
|
||||||
|
): Response
|
||||||
|
{
|
||||||
|
$user = $security->getUser();
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$player = $playerRepository->findOneBy(['session' => $session, 'user' => $user]);
|
||||||
|
|
||||||
|
if ($request->isMethod('POST') && $request->request->has('toggle_ready')) {
|
||||||
|
$dashboardService->toggleReady($session, $user);
|
||||||
|
return $this->redirectToRoute('game', ['session' => $session->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodically check readiness timeout
|
||||||
|
$dashboardService->checkAllPlayersReady($session);
|
||||||
|
|
||||||
|
if ($session->getStatus() === SessionStatus::READY) {
|
||||||
|
$isReady = false;
|
||||||
|
$readyAt = null;
|
||||||
|
if ($player) {
|
||||||
|
$settingName = SessionSettingType::tryFrom('ReadyAtForPlayer' . $player->getScreen());
|
||||||
|
if ($settingName) {
|
||||||
|
$setting = $session->getSettings()->filter(fn(SessionSetting $s) => $s->getName() === $settingName && $s->getPlayer() === $player)->first();
|
||||||
|
if ($setting) {
|
||||||
|
$isReady = true;
|
||||||
|
$readyAt = (int)$setting->getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('game/waiting.html.twig', [
|
||||||
|
'session' => $session,
|
||||||
|
'isReady' => $isReady,
|
||||||
|
'readyAt' => $readyAt,
|
||||||
|
'mercure_public_url' => $this->mercurePublicUrl,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$screen = $player ? $player->getScreen() : 0;
|
||||||
|
$session_id = $session->getId();
|
||||||
|
|
||||||
|
return $this->render('game/index.html.twig', [
|
||||||
|
'session' => $session,
|
||||||
|
'screen' => $screen,
|
||||||
|
'session_id' => $session_id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/lost/{session}', name: 'game_lost', methods: ['GET', 'POST'])]
|
||||||
|
#[IsGranted(new Expression("is_granted('ROLE_PLAYER') or is_granted('ROLE_ADMIN')"))]
|
||||||
|
#[IsGranted('SESSION_VIEW', subject: 'session')]
|
||||||
|
public function lost(
|
||||||
|
Session $session,
|
||||||
|
Request $request,
|
||||||
|
Security $security,
|
||||||
|
PlayerService $playerService,
|
||||||
|
GameDashboardService $dashboardService
|
||||||
|
): Response {
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $security->getUser();
|
||||||
|
$player = $playerService->GetCurrentlyActiveAsPlayer($user);
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$difficulty = $request->request->get('difficulty');
|
||||||
|
$entertaining = $request->request->get('entertaining');
|
||||||
|
$theme = $request->request->get('theme');
|
||||||
|
$feedback = $request->request->get('feedback');
|
||||||
|
|
||||||
|
// Save feedback
|
||||||
|
if ($player) {
|
||||||
|
$this->saveFeedback($session, $player, $difficulty, $entertaining, $theme, $feedback);
|
||||||
|
$this->addFlash('success', 'Thank you for your feedback!');
|
||||||
|
return $this->redirectToRoute('game_dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('game/lost.html.twig', [
|
||||||
|
'session' => $session,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function saveFeedback(Session $session, Player $player, $difficulty, $entertaining, $theme, $feedback): void
|
||||||
|
{
|
||||||
|
$settings = [
|
||||||
|
SessionSettingType::FEEDBACK_DIFFICULTY,
|
||||||
|
SessionSettingType::FEEDBACK_ENTERTAINING,
|
||||||
|
SessionSettingType::FEEDBACK_THEME,
|
||||||
|
SessionSettingType::FEEDBACK_TEXT,
|
||||||
|
];
|
||||||
|
|
||||||
|
$values = [
|
||||||
|
$difficulty,
|
||||||
|
$entertaining,
|
||||||
|
$theme,
|
||||||
|
$feedback,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($settings as $index => $type) {
|
||||||
|
$value = $values[$index];
|
||||||
|
if ($value === null || $value === '') continue;
|
||||||
|
|
||||||
|
$setting = new SessionSetting();
|
||||||
|
$setting->setSession($session);
|
||||||
|
$setting->setPlayer($player);
|
||||||
|
$setting->setName($type);
|
||||||
|
$setting->setValue((string)$value);
|
||||||
|
$this->entityManager->persist($setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
141
src/Game/Entity/Game.php
Normal file
141
src/Game/Entity/Game.php
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Entity;
|
||||||
|
|
||||||
|
use App\Game\Enum\GameStatus;
|
||||||
|
use App\Game\Repository\GameRepository;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: GameRepository::class)]
|
||||||
|
#[ORM\Table(name: 'game')]
|
||||||
|
class Game
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $name = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $numberOfPlayers = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 20, enumType: GameStatus::class)]
|
||||||
|
private ?GameStatus $status = null;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'game', targetEntity: Session::class)]
|
||||||
|
private Collection $sessions;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'game', targetEntity: GameSetting::class)]
|
||||||
|
private Collection $settings;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->sessions = new ArrayCollection();
|
||||||
|
$this->settings = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): ?string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNumberOfPlayers(): ?int
|
||||||
|
{
|
||||||
|
return $this->numberOfPlayers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNumberOfPlayers(int $numberOfPlayers): static
|
||||||
|
{
|
||||||
|
$this->numberOfPlayers = $numberOfPlayers;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): ?GameStatus
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(GameStatus $status): static
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Session>
|
||||||
|
*/
|
||||||
|
public function getSessions(): Collection
|
||||||
|
{
|
||||||
|
return $this->sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addSession(Session $session): static
|
||||||
|
{
|
||||||
|
if (!$this->sessions->contains($session)) {
|
||||||
|
$this->sessions->add($session);
|
||||||
|
$session->setGame($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeSession(Session $session): static
|
||||||
|
{
|
||||||
|
if ($this->sessions->removeElement($session)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($session->getGame() === $this) {
|
||||||
|
$session->setGame(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, GameSetting>
|
||||||
|
*/
|
||||||
|
public function getSettings(): Collection
|
||||||
|
{
|
||||||
|
return $this->settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addSetting(GameSetting $setting): static
|
||||||
|
{
|
||||||
|
if (!$this->settings->contains($setting)) {
|
||||||
|
$this->settings->add($setting);
|
||||||
|
$setting->setGame($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeSetting(GameSetting $setting): static
|
||||||
|
{
|
||||||
|
if ($this->settings->removeElement($setting)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($setting->getGame() === $this) {
|
||||||
|
$setting->setGame(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/Game/Entity/GameSetting.php
Normal file
68
src/Game/Entity/GameSetting.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Entity;
|
||||||
|
|
||||||
|
use App\Game\Enum\GameSettingType;
|
||||||
|
use App\Game\Repository\GameSettingRepository;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: GameSettingRepository::class)]
|
||||||
|
#[ORM\Table(name: 'game_setting')]
|
||||||
|
class GameSetting
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Game::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private ?Game $game = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 255, enumType: GameSettingType::class)]
|
||||||
|
private ?GameSettingType $name = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
|
private ?string $value = null;
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGame(): ?Game
|
||||||
|
{
|
||||||
|
return $this->game;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setGame(?Game $game): static
|
||||||
|
{
|
||||||
|
$this->game = $game;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): ?GameSettingType
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(GameSettingType $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getValue(): ?string
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setValue(?string $value): static
|
||||||
|
{
|
||||||
|
$this->value = $value;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/Game/Entity/Player.php
Normal file
69
src/Game/Entity/Player.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Entity;
|
||||||
|
|
||||||
|
use App\Game\Repository\PlayerRepository;
|
||||||
|
use App\Tech\Entity\User;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: PlayerRepository::class)]
|
||||||
|
#[ORM\Table(name: 'player')]
|
||||||
|
class Player
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Session::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private ?Session $session = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private ?User $user = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?int $screen = null;
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSession(): ?Session
|
||||||
|
{
|
||||||
|
return $this->session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSession(?Session $session): static
|
||||||
|
{
|
||||||
|
$this->session = $session;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): ?User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUser(?User $user): static
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getScreen(): ?int
|
||||||
|
{
|
||||||
|
return $this->screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setScreen(?int $screen): static
|
||||||
|
{
|
||||||
|
$this->screen = $screen;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
159
src/Game/Entity/Session.php
Normal file
159
src/Game/Entity/Session.php
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Entity;
|
||||||
|
|
||||||
|
use App\Game\Enum\SessionStatus;
|
||||||
|
use App\Game\Repository\SessionRepository;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: SessionRepository::class)]
|
||||||
|
#[ORM\Table(name: 'session')]
|
||||||
|
class Session
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Game::class, inversedBy: 'sessions')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private ?Game $game = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 20, enumType: SessionStatus::class)]
|
||||||
|
private ?SessionStatus $status = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $timer = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
|
||||||
|
private ?\DateTimeInterface $created = null;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'session', targetEntity: Player::class)]
|
||||||
|
private Collection $players;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'session', targetEntity: SessionSetting::class)]
|
||||||
|
private Collection $settings;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->created = new \DateTime();
|
||||||
|
$this->players = new ArrayCollection();
|
||||||
|
$this->settings = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGame(): ?Game
|
||||||
|
{
|
||||||
|
return $this->game;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setGame(?Game $game): static
|
||||||
|
{
|
||||||
|
$this->game = $game;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): ?SessionStatus
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(SessionStatus $status): static
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTimer(): ?int
|
||||||
|
{
|
||||||
|
return $this->timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTimer(int $timer): static
|
||||||
|
{
|
||||||
|
$this->timer = $timer;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreated(): ?\DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->created;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreated(\DateTimeInterface $created): static
|
||||||
|
{
|
||||||
|
$this->created = $created;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Player>
|
||||||
|
*/
|
||||||
|
public function getPlayers(): Collection
|
||||||
|
{
|
||||||
|
return $this->players;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addPlayer(Player $player): static
|
||||||
|
{
|
||||||
|
if (!$this->players->contains($player)) {
|
||||||
|
$this->players->add($player);
|
||||||
|
$player->setSession($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removePlayer(Player $player): static
|
||||||
|
{
|
||||||
|
if ($this->players->removeElement($player)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($player->getSession() === $this) {
|
||||||
|
$player->setSession(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, SessionSetting>
|
||||||
|
*/
|
||||||
|
public function getSettings(): Collection
|
||||||
|
{
|
||||||
|
return $this->settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addSetting(SessionSetting $setting): static
|
||||||
|
{
|
||||||
|
if (!$this->settings->contains($setting)) {
|
||||||
|
$this->settings->add($setting);
|
||||||
|
$setting->setSession($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeSetting(SessionSetting $setting): static
|
||||||
|
{
|
||||||
|
if ($this->settings->removeElement($setting)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($setting->getSession() === $this) {
|
||||||
|
$setting->setSession(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/Game/Entity/SessionSetting.php
Normal file
84
src/Game/Entity/SessionSetting.php
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Entity;
|
||||||
|
|
||||||
|
use App\Game\Enum\SessionSettingType;
|
||||||
|
use App\Game\Repository\SessionSettingRepository;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: SessionSettingRepository::class)]
|
||||||
|
#[ORM\Table(name: 'session_setting')]
|
||||||
|
class SessionSetting
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Session::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private ?Session $session = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Player::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
private ?Player $player = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 255, enumType: SessionSettingType::class)]
|
||||||
|
private ?SessionSettingType $name = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
|
private ?string $value = null;
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSession(): ?Session
|
||||||
|
{
|
||||||
|
return $this->session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSession(?Session $session): static
|
||||||
|
{
|
||||||
|
$this->session = $session;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPlayer(): ?Player
|
||||||
|
{
|
||||||
|
return $this->player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPlayer(?Player $player): static
|
||||||
|
{
|
||||||
|
$this->player = $player;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): ?SessionSettingType
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(SessionSettingType $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getValue(): ?string
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setValue(?string $value): static
|
||||||
|
{
|
||||||
|
$this->value = $value;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/Game/Enum/DecodeMessage.php
Normal file
10
src/Game/Enum/DecodeMessage.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Enum;
|
||||||
|
|
||||||
|
enum DecodeMessage: string
|
||||||
|
{
|
||||||
|
case TEST = 'This is a test decoding message.';
|
||||||
|
case SECRET = 'The secret code is 42.';
|
||||||
|
case WELCOME = 'Welcome to the system, agent.';
|
||||||
|
}
|
||||||
8
src/Game/Enum/GameSettingType.php
Normal file
8
src/Game/Enum/GameSettingType.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Enum;
|
||||||
|
|
||||||
|
enum GameSettingType: string
|
||||||
|
{
|
||||||
|
case TOTAL_TIME = 'totalTime';
|
||||||
|
}
|
||||||
10
src/Game/Enum/GameStatus.php
Normal file
10
src/Game/Enum/GameStatus.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Enum;
|
||||||
|
|
||||||
|
enum GameStatus: string
|
||||||
|
{
|
||||||
|
case IN_DEVELOPMENT = 'inDevelopment';
|
||||||
|
case LOCKED = 'locked';
|
||||||
|
case OPEN = 'open';
|
||||||
|
}
|
||||||
77
src/Game/Enum/SessionSettingType.php
Normal file
77
src/Game/Enum/SessionSettingType.php
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Enum;
|
||||||
|
|
||||||
|
enum SessionSettingType: string
|
||||||
|
{
|
||||||
|
case PWD_FOR_PLAYER1 = 'PwdForPlayer1';
|
||||||
|
case PWD_FOR_PLAYER2 = 'PwdForPlayer2';
|
||||||
|
case PWD_FOR_PLAYER3 = 'PwdForPlayer3';
|
||||||
|
case PWD_FOR_PLAYER4 = 'PwdForPlayer4';
|
||||||
|
case PWD_FOR_PLAYER5 = 'PwdForPlayer5';
|
||||||
|
case PWD_FOR_PLAYER6 = 'PwdForPlayer6';
|
||||||
|
case PWD_FOR_PLAYER7 = 'PwdForPlayer7';
|
||||||
|
case PWD_FOR_PLAYER8 = 'PwdForPlayer8';
|
||||||
|
case PWD_FOR_PLAYER9 = 'PwdForPlayer9';
|
||||||
|
case PWD_FOR_PLAYER10 = 'PwdForPlayer10';
|
||||||
|
case RIGHTS_FOR_PLAYER1 = 'RightsForPlayer1';
|
||||||
|
case RIGHTS_FOR_PLAYER2 = 'RightsForPlayer2';
|
||||||
|
case RIGHTS_FOR_PLAYER3 = 'RightsForPlayer3';
|
||||||
|
case RIGHTS_FOR_PLAYER4 = 'RightsForPlayer4';
|
||||||
|
case RIGHTS_FOR_PLAYER5 = 'RightsForPlayer5';
|
||||||
|
case RIGHTS_FOR_PLAYER6 = 'RightsForPlayer6';
|
||||||
|
case RIGHTS_FOR_PLAYER7 = 'RightsForPlayer7';
|
||||||
|
case RIGHTS_FOR_PLAYER8 = 'RightsForPlayer8';
|
||||||
|
case RIGHTS_FOR_PLAYER9 = 'RightsForPlayer9';
|
||||||
|
case RIGHTS_FOR_PLAYER10 = 'RightsForPlayer10';
|
||||||
|
case INVITE_CODE = 'InviteCode';
|
||||||
|
case SET_OF_DELETED_FILES = 'SetOfDeletedFiles';
|
||||||
|
case CHAT_TRACKING_FOR_PLAYER1 = 'ChatTrackingForPlayer1';
|
||||||
|
case CHAT_TRACKING_FOR_PLAYER2 = 'ChatTrackingForPlayer2';
|
||||||
|
case CHAT_TRACKING_FOR_PLAYER3 = 'ChatTrackingForPlayer3';
|
||||||
|
case CHAT_TRACKING_FOR_PLAYER4 = 'ChatTrackingForPlayer4';
|
||||||
|
case CHAT_TRACKING_FOR_PLAYER5 = 'ChatTrackingForPlayer5';
|
||||||
|
case CHAT_TRACKING_FOR_PLAYER6 = 'ChatTrackingForPlayer6';
|
||||||
|
case CHAT_TRACKING_FOR_PLAYER7 = 'ChatTrackingForPlayer7';
|
||||||
|
case CHAT_TRACKING_FOR_PLAYER8 = 'ChatTrackingForPlayer8';
|
||||||
|
case CHAT_TRACKING_FOR_PLAYER9 = 'ChatTrackingForPlayer9';
|
||||||
|
case CHAT_TRACKING_FOR_PLAYER10 = 'ChatTrackingForPlayer10';
|
||||||
|
case VERIFY_CODES_FOR_PLAYER1 = 'VerifyCodesForPlayer1';
|
||||||
|
case VERIFY_CODES_FOR_PLAYER2 = 'VerifyCodesForPlayer2';
|
||||||
|
case VERIFY_CODES_FOR_PLAYER3 = 'VerifyCodesForPlayer3';
|
||||||
|
case VERIFY_CODES_FOR_PLAYER4 = 'VerifyCodesForPlayer4';
|
||||||
|
case VERIFY_CODES_FOR_PLAYER5 = 'VerifyCodesForPlayer5';
|
||||||
|
case VERIFY_CODES_FOR_PLAYER6 = 'VerifyCodesForPlayer6';
|
||||||
|
case VERIFY_CODES_FOR_PLAYER7 = 'VerifyCodesForPlayer7';
|
||||||
|
case VERIFY_CODES_FOR_PLAYER8 = 'VerifyCodesForPlayer8';
|
||||||
|
case VERIFY_CODES_FOR_PLAYER9 = 'VerifyCodesForPlayer9';
|
||||||
|
case VERIFY_CODES_FOR_PLAYER10 = 'VerifyCodesForPlayer10';
|
||||||
|
case VERIFICATION_PROGRESS_FOR_PLAYER1 = 'VerificationProgressForPlayer1';
|
||||||
|
case VERIFICATION_PROGRESS_FOR_PLAYER2 = 'VerificationProgressForPlayer2';
|
||||||
|
case VERIFICATION_PROGRESS_FOR_PLAYER3 = 'VerificationProgressForPlayer3';
|
||||||
|
case VERIFICATION_PROGRESS_FOR_PLAYER4 = 'VerificationProgressForPlayer4';
|
||||||
|
case VERIFICATION_PROGRESS_FOR_PLAYER5 = 'VerificationProgressForPlayer5';
|
||||||
|
case VERIFICATION_PROGRESS_FOR_PLAYER6 = 'VerificationProgressForPlayer6';
|
||||||
|
case VERIFICATION_PROGRESS_FOR_PLAYER7 = 'VerificationProgressForPlayer7';
|
||||||
|
case VERIFICATION_PROGRESS_FOR_PLAYER8 = 'VerificationProgressForPlayer8';
|
||||||
|
case VERIFICATION_PROGRESS_FOR_PLAYER9 = 'VerificationProgressForPlayer9';
|
||||||
|
case VERIFICATION_PROGRESS_FOR_PLAYER10 = 'VerificationProgressForPlayer10';
|
||||||
|
case EVERYONE_VERIFIED = 'EveryoneVerified';
|
||||||
|
case SPECIAL_REPORT_CODE_DOYLE = 'SpecialReportCodeDoyle';
|
||||||
|
case SPECIAL_REPORT_CODE_VEGA = 'SpecialReportCodeVega';
|
||||||
|
case SPECIAL_REPORT_CODE_LENNOX = 'SpecialReportCodeLennox';
|
||||||
|
case READY_AT_FOR_PLAYER1 = 'ReadyAtForPlayer1';
|
||||||
|
case READY_AT_FOR_PLAYER2 = 'ReadyAtForPlayer2';
|
||||||
|
case READY_AT_FOR_PLAYER3 = 'ReadyAtForPlayer3';
|
||||||
|
case READY_AT_FOR_PLAYER4 = 'ReadyAtForPlayer4';
|
||||||
|
case READY_AT_FOR_PLAYER5 = 'ReadyAtForPlayer5';
|
||||||
|
case READY_AT_FOR_PLAYER6 = 'ReadyAtForPlayer6';
|
||||||
|
case READY_AT_FOR_PLAYER7 = 'ReadyAtForPlayer7';
|
||||||
|
case READY_AT_FOR_PLAYER8 = 'ReadyAtForPlayer8';
|
||||||
|
case READY_AT_FOR_PLAYER9 = 'ReadyAtForPlayer9';
|
||||||
|
case READY_AT_FOR_PLAYER10 = 'ReadyAtForPlayer10';
|
||||||
|
case FEEDBACK_DIFFICULTY = 'FeedbackDifficulty';
|
||||||
|
case FEEDBACK_ENTERTAINING = 'FeedbackEntertaining';
|
||||||
|
case FEEDBACK_THEME = 'FeedbackTheme';
|
||||||
|
case FEEDBACK_TEXT = 'FeedbackText';
|
||||||
|
}
|
||||||
12
src/Game/Enum/SessionStatus.php
Normal file
12
src/Game/Enum/SessionStatus.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Enum;
|
||||||
|
|
||||||
|
enum SessionStatus: string
|
||||||
|
{
|
||||||
|
case CREATED = 'created';
|
||||||
|
case READY = 'ready';
|
||||||
|
case PLAYING = 'playing';
|
||||||
|
case WON = 'won';
|
||||||
|
case LOST = 'lost';
|
||||||
|
}
|
||||||
18
src/Game/Repository/GameRepository.php
Normal file
18
src/Game/Repository/GameRepository.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Repository;
|
||||||
|
|
||||||
|
use App\Game\Entity\Game;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<Game>
|
||||||
|
*/
|
||||||
|
class GameRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, Game::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/Game/Repository/GameSettingRepository.php
Normal file
28
src/Game/Repository/GameSettingRepository.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Repository;
|
||||||
|
|
||||||
|
use App\Game\Entity\Game;
|
||||||
|
use App\Game\Entity\GameSetting;
|
||||||
|
use App\Game\Enum\GameSettingType;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<GameSetting>
|
||||||
|
*/
|
||||||
|
class GameSettingRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, GameSetting::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSetting(Game $game, GameSettingType $name): ?GameSetting
|
||||||
|
{
|
||||||
|
return $this->findOneBy([
|
||||||
|
'game' => $game,
|
||||||
|
'name' => $name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/Game/Repository/PlayerRepository.php
Normal file
18
src/Game/Repository/PlayerRepository.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Repository;
|
||||||
|
|
||||||
|
use App\Game\Entity\Player;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<Player>
|
||||||
|
*/
|
||||||
|
class PlayerRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, Player::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/Game/Repository/SessionRepository.php
Normal file
18
src/Game/Repository/SessionRepository.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Repository;
|
||||||
|
|
||||||
|
use App\Game\Entity\Session;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<Session>
|
||||||
|
*/
|
||||||
|
class SessionRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, Session::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/Game/Repository/SessionSettingRepository.php
Normal file
39
src/Game/Repository/SessionSettingRepository.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Repository;
|
||||||
|
|
||||||
|
use App\Game\Enum\SessionSettingType;
|
||||||
|
use App\Game\Entity\Player;
|
||||||
|
use App\Game\Entity\Session;
|
||||||
|
use App\Game\Entity\SessionSetting;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<SessionSetting>
|
||||||
|
*/
|
||||||
|
class SessionSettingRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, SessionSetting::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSetting(Session $session, SessionSettingType $name, ?Player $player = null): ?SessionSetting
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('s')
|
||||||
|
->where('s.session = :session')
|
||||||
|
->andWhere('s.name = :name')
|
||||||
|
->setParameter('session', $session)
|
||||||
|
->setParameter('name', $name->value);
|
||||||
|
|
||||||
|
if ($player) {
|
||||||
|
$qb->andWhere('s.player = :player')
|
||||||
|
->setParameter('player', $player);
|
||||||
|
} else {
|
||||||
|
$qb->andWhere('s.player IS NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb->getQuery()->getOneOrNullResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/Game/Security/Voter/SessionVoter.php
Normal file
48
src/Game/Security/Voter/SessionVoter.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Security\Voter;
|
||||||
|
|
||||||
|
use App\Game\Entity\Session;
|
||||||
|
use App\Tech\Entity\User;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
|
class SessionVoter extends Voter
|
||||||
|
{
|
||||||
|
public const VIEW = 'SESSION_VIEW';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly Security $security,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function supports(string $attribute, mixed $subject): bool
|
||||||
|
{
|
||||||
|
return $attribute === self::VIEW && $subject instanceof Session;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||||
|
{
|
||||||
|
$user = $token->getUser();
|
||||||
|
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->security->isGranted('ROLE_ADMIN')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Session $session */
|
||||||
|
$session = $subject;
|
||||||
|
|
||||||
|
foreach ($session->getPlayers() as $player) {
|
||||||
|
if ($player->getUser() === $user) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
464
src/Game/Service/GameDashboardService.php
Normal file
464
src/Game/Service/GameDashboardService.php
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Game\Service;
|
||||||
|
|
||||||
|
use App\Game\Entity\Game;
|
||||||
|
use App\Game\Entity\Player;
|
||||||
|
use App\Game\Entity\Session;
|
||||||
|
use App\Game\Entity\SessionSetting;
|
||||||
|
use App\Game\Enum\GameStatus;
|
||||||
|
use App\Game\Enum\SessionSettingType;
|
||||||
|
use App\Game\Enum\SessionStatus;
|
||||||
|
use App\Game\Repository\GameRepository;
|
||||||
|
use App\Game\Repository\SessionRepository;
|
||||||
|
use App\Tech\Entity\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
use Symfony\Component\Mercure\HubInterface;
|
||||||
|
use Symfony\Component\Mercure\Update;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
|
||||||
|
final class GameDashboardService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly GameRepository $gameRepository,
|
||||||
|
private readonly SessionRepository $sessionRepository,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly HubInterface $hub,
|
||||||
|
#[Autowire('%env(MERCURE_TOPIC_BASE)%')]
|
||||||
|
private readonly string $mercureTopicBase,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Session[]
|
||||||
|
*/
|
||||||
|
public function getSessionsForUser(UserInterface $user): array
|
||||||
|
{
|
||||||
|
return $this->sessionRepository->createQueryBuilder('s')
|
||||||
|
->innerJoin('s.players', 'p')
|
||||||
|
->where('p.user = :user')
|
||||||
|
->setParameter('user', $user)
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Game[]
|
||||||
|
*/
|
||||||
|
public function getAvailableGames(bool $isAdmin): array
|
||||||
|
{
|
||||||
|
if ($isAdmin) {
|
||||||
|
return $this->gameRepository->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->gameRepository->findBy(['status' => GameStatus::OPEN]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createSession(Game $game, UserInterface $user, bool $isAdmin): ?Session
|
||||||
|
{
|
||||||
|
if ($game->getStatus() !== GameStatus::OPEN && !$isAdmin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$user instanceof User)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
$session = new Session();
|
||||||
|
$session->setGame($game);
|
||||||
|
$session->setStatus(SessionStatus::CREATED);
|
||||||
|
$session->setTimer(0);
|
||||||
|
|
||||||
|
$player = new Player();
|
||||||
|
$player->setUser($user);
|
||||||
|
$session->addPlayer($player);
|
||||||
|
|
||||||
|
$this->entityManager->persist($session);
|
||||||
|
$this->entityManager->persist($player);
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
if (count($session->getPlayers()) === $session->getGame()->getNumberOfPlayers()) {
|
||||||
|
$this->startSession($session);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function joinSession(string $inviteCode, UserInterface $user): bool
|
||||||
|
{
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting = $this->entityManager->getRepository(SessionSetting::class)->findOneBy([
|
||||||
|
'name' => SessionSettingType::INVITE_CODE,
|
||||||
|
'value' => $inviteCode,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$setting) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$session = $setting->getSession();
|
||||||
|
|
||||||
|
// Check if user is already in this session
|
||||||
|
foreach ($session->getPlayers() as $player) {
|
||||||
|
if ($player->getUser() === $user) {
|
||||||
|
return true; // Already joined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$playerCount = count($session->getPlayers());
|
||||||
|
if ($playerCount >= $session->getGame()->getNumberOfPlayers()) {
|
||||||
|
return false; // Session full
|
||||||
|
}
|
||||||
|
|
||||||
|
$player = new Player();
|
||||||
|
$player->setUser($user);
|
||||||
|
$session->addPlayer($player);
|
||||||
|
|
||||||
|
$this->entityManager->persist($player);
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
if (count($session->getPlayers()) === $session->getGame()->getNumberOfPlayers()) {
|
||||||
|
$this->startSession($session);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function leaveSession(Session $session, UserInterface $user): bool
|
||||||
|
{
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($session->getStatus(), [SessionStatus::CREATED, SessionStatus::READY]) || $session->getTimer() > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$playerToDelete = null;
|
||||||
|
foreach ($session->getPlayers() as $player) {
|
||||||
|
if ($player->getUser() === $user) {
|
||||||
|
$playerToDelete = $player;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$playerToDelete) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($session->getStatus() === SessionStatus::READY) {
|
||||||
|
$session->setStatus(SessionStatus::CREATED);
|
||||||
|
// Clear assignments for all remaining players since the game needs to be "started" again
|
||||||
|
foreach ($session->getPlayers() as $player) {
|
||||||
|
$player->setScreen(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove player specific settings (like rights)
|
||||||
|
foreach ($session->getSettings() as $setting) {
|
||||||
|
if ($setting->getPlayer() === $playerToDelete) {
|
||||||
|
$session->removeSetting($setting);
|
||||||
|
$this->entityManager->remove($setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$session->removePlayer($playerToDelete);
|
||||||
|
$this->entityManager->remove($playerToDelete);
|
||||||
|
|
||||||
|
// If no players left, we might want to delete the session and its remaining settings
|
||||||
|
if ($session->getPlayers()->isEmpty()) {
|
||||||
|
foreach ($session->getSettings() as $setting) {
|
||||||
|
$this->entityManager->remove($setting);
|
||||||
|
}
|
||||||
|
$this->entityManager->remove($session);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function initializePlayerSettings(Player $player): void
|
||||||
|
{
|
||||||
|
$screen = $player->getScreen();
|
||||||
|
$rightsSettingName = SessionSettingType::tryFrom('RightsForPlayer' . $screen);
|
||||||
|
|
||||||
|
if ($rightsSettingName) {
|
||||||
|
$setting = new SessionSetting();
|
||||||
|
$setting->setSession($player->getSession());
|
||||||
|
$setting->setPlayer($player);
|
||||||
|
$setting->setName($rightsSettingName);
|
||||||
|
$setting->setValue(json_encode(['chat', 'help', 'ls', 'pwd']));
|
||||||
|
$this->entityManager->persist($setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pwdSettingName = SessionSettingType::tryFrom('PwdForPlayer' . $screen);
|
||||||
|
|
||||||
|
if ($pwdSettingName) {
|
||||||
|
$setting = new SessionSetting();
|
||||||
|
$setting->setSession($player->getSession());
|
||||||
|
$setting->setPlayer($player);
|
||||||
|
$setting->setName($pwdSettingName);
|
||||||
|
$setting->setValue('var/home/' . $player->getUser()->getUsername());
|
||||||
|
$this->entityManager->persist($setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
$chatTrackingSettingName = SessionSettingType::tryFrom('ChatTrackingForPlayer' . $screen);
|
||||||
|
|
||||||
|
if ($chatTrackingSettingName) {
|
||||||
|
$setting = new SessionSetting();
|
||||||
|
$setting->setSession($player->getSession());
|
||||||
|
$setting->setPlayer($player);
|
||||||
|
$setting->setName($chatTrackingSettingName);
|
||||||
|
$setting->setValue(json_encode([]));
|
||||||
|
$this->entityManager->persist($setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
$verifyCodesSettingName = SessionSettingType::tryFrom('VerifyCodesForPlayer' . $screen);
|
||||||
|
|
||||||
|
if ($verifyCodesSettingName) {
|
||||||
|
$codes = [];
|
||||||
|
$numPlayers = $player->getSession()->getGame()->getNumberOfPlayers();
|
||||||
|
for ($i = 1; $i <= $numPlayers; $i++) {
|
||||||
|
if ($i !== $screen) {
|
||||||
|
$codes[$i] = bin2hex(random_bytes(3)); // 6 characters code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting = new SessionSetting();
|
||||||
|
$setting->setSession($player->getSession());
|
||||||
|
$setting->setPlayer($player);
|
||||||
|
$setting->setName($verifyCodesSettingName);
|
||||||
|
$setting->setValue(json_encode($codes));
|
||||||
|
$this->entityManager->persist($setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
$verificationProgressSettingName = SessionSettingType::tryFrom('VerificationProgressForPlayer' . $screen);
|
||||||
|
|
||||||
|
if ($verificationProgressSettingName) {
|
||||||
|
$setting = new SessionSetting();
|
||||||
|
$setting->setSession($player->getSession());
|
||||||
|
$setting->setPlayer($player);
|
||||||
|
$setting->setName($verificationProgressSettingName);
|
||||||
|
$setting->setValue(json_encode([]));
|
||||||
|
$this->entityManager->persist($setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function startSession(Session $session): bool
|
||||||
|
{
|
||||||
|
if ($session->getStatus() !== SessionStatus::CREATED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$players = $session->getPlayers()->toArray();
|
||||||
|
if (count($players) < $session->getGame()->getNumberOfPlayers()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up any existing assignments (e.g. if we reverted from READY to CREATED)
|
||||||
|
foreach ($session->getSettings() as $setting) {
|
||||||
|
if ($setting->getPlayer() !== null) {
|
||||||
|
$session->removeSetting($setting);
|
||||||
|
$this->entityManager->remove($setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle players to assign random screens
|
||||||
|
shuffle($players);
|
||||||
|
|
||||||
|
foreach ($players as $index => $player) {
|
||||||
|
$screen = $index + 1;
|
||||||
|
$player->setScreen($screen);
|
||||||
|
$this->initializePlayerSettings($player);
|
||||||
|
$this->entityManager->persist($player);
|
||||||
|
}
|
||||||
|
|
||||||
|
$session->setStatus(SessionStatus::READY);
|
||||||
|
$this->entityManager->persist($session);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toggleReady(Session $session, User $user): bool
|
||||||
|
{
|
||||||
|
if ($session->getStatus() !== SessionStatus::READY) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$player = null;
|
||||||
|
foreach ($session->getPlayers() as $p) {
|
||||||
|
if ($p->getUser() === $user) {
|
||||||
|
$player = $p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$player) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$settingName = SessionSettingType::tryFrom('ReadyAtForPlayer' . $player->getScreen());
|
||||||
|
if (!$settingName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var \App\Game\Repository\SessionSettingRepository $settingRepo */
|
||||||
|
$settingRepo = $this->entityManager->getRepository(SessionSetting::class);
|
||||||
|
$setting = $settingRepo->getSetting($session, $settingName, $player);
|
||||||
|
|
||||||
|
if ($setting) {
|
||||||
|
$session->removeSetting($setting);
|
||||||
|
$this->entityManager->remove($setting);
|
||||||
|
} else {
|
||||||
|
$setting = new SessionSetting();
|
||||||
|
$setting->setSession($session);
|
||||||
|
$setting->setPlayer($player);
|
||||||
|
$setting->setName($settingName);
|
||||||
|
$setting->setValue((string)(new \DateTime())->getTimestamp());
|
||||||
|
$this->entityManager->persist($setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->checkAllPlayersReady($session);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$topic = $this->mercureTopicBase . '/game/hub-' . $session->getId();
|
||||||
|
$this->hub->publish(new Update($topic, json_encode(['type' => 'player_ready', 'player' => $player->getScreen(), 'ready' => !$setting])));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Mercure might be down, but we don't want to crash the game
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkAllPlayersReady(Session $session): void
|
||||||
|
{
|
||||||
|
if ($session->getStatus() !== SessionStatus::READY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$players = $session->getPlayers();
|
||||||
|
$numPlayers = $session->getGame()->getNumberOfPlayers();
|
||||||
|
|
||||||
|
if (count($players) < $numPlayers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$readyPlayersCount = 0;
|
||||||
|
$now = new \DateTime();
|
||||||
|
$anyReset = false;
|
||||||
|
|
||||||
|
/** @var \App\Game\Repository\SessionSettingRepository $settingRepo */
|
||||||
|
$settingRepo = $this->entityManager->getRepository(SessionSetting::class);
|
||||||
|
|
||||||
|
foreach ($players as $player) {
|
||||||
|
$settingName = SessionSettingType::tryFrom('ReadyAtForPlayer' . $player->getScreen());
|
||||||
|
if (!$settingName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting = $settingRepo->getSetting($session, $settingName, $player);
|
||||||
|
if ($setting) {
|
||||||
|
$readyAtTimestamp = (int)$setting->getValue();
|
||||||
|
// Check timeout: 1 minute = 60 seconds
|
||||||
|
if (($now->getTimestamp() - $readyAtTimestamp) > 60) {
|
||||||
|
$session->removeSetting($setting);
|
||||||
|
$this->entityManager->remove($setting);
|
||||||
|
$anyReset = true;
|
||||||
|
} else {
|
||||||
|
$readyPlayersCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($anyReset) {
|
||||||
|
$this->entityManager->flush();
|
||||||
|
try {
|
||||||
|
$topic = $this->mercureTopicBase . '/game/hub-' . $session->getId();
|
||||||
|
$this->hub->publish(new Update($topic, json_encode(['type' => 'player_ready'])));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Mercure might be down
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($readyPlayersCount === $numPlayers) {
|
||||||
|
$session->setStatus(SessionStatus::PLAYING);
|
||||||
|
|
||||||
|
// Set the end timer
|
||||||
|
$game = $session->getGame();
|
||||||
|
/** @var \App\Game\Repository\GameSettingRepository $gameSettingRepo */
|
||||||
|
$gameSettingRepo = $this->entityManager->getRepository(\App\Game\Entity\GameSetting::class);
|
||||||
|
$totalTimeSetting = $gameSettingRepo->getSetting($game, \App\Game\Enum\GameSettingType::TOTAL_TIME);
|
||||||
|
$totalTime = $totalTimeSetting ? (int)$totalTimeSetting->getValue() : 3600; // Default to 60 minutes if not set
|
||||||
|
|
||||||
|
$session->setTimer((new \DateTime())->getTimestamp() + $totalTime);
|
||||||
|
|
||||||
|
$this->entityManager->persist($session);
|
||||||
|
|
||||||
|
// Clean up ready settings
|
||||||
|
foreach ($players as $player) {
|
||||||
|
$settingName = SessionSettingType::tryFrom('ReadyAtForPlayer' . $player->getScreen());
|
||||||
|
if ($settingName) {
|
||||||
|
$setting = $settingRepo->getSetting($session, $settingName, $player);
|
||||||
|
if ($setting) {
|
||||||
|
$session->removeSetting($setting);
|
||||||
|
$this->entityManager->remove($setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$topic = $this->mercureTopicBase . '/game/hub-' . $session->getId();
|
||||||
|
$this->hub->publish(new Update($topic, json_encode(['type' => 'all_ready'])));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Mercure might be down, but we don't want to crash the game
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateInviteCode(Session $session, UserInterface $user, bool $isAdmin): ?string
|
||||||
|
{
|
||||||
|
// Security check: is user part of this session?
|
||||||
|
$isPlayer = false;
|
||||||
|
foreach ($session->getPlayers() as $player) {
|
||||||
|
if ($player->getUser() === $user) {
|
||||||
|
$isPlayer = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$isPlayer && !$isAdmin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$inviteCode = bin2hex(random_bytes(4));
|
||||||
|
|
||||||
|
$setting = null;
|
||||||
|
foreach ($session->getSettings() as $s) {
|
||||||
|
if ($s->getName() === SessionSettingType::INVITE_CODE) {
|
||||||
|
$setting = $s;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$setting) {
|
||||||
|
$setting = new SessionSetting();
|
||||||
|
$setting->setSession($session);
|
||||||
|
$setting->setName(SessionSettingType::INVITE_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting->setValue($inviteCode);
|
||||||
|
$this->entityManager->persist($setting);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $inviteCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
96
src/Game/Service/PlayerService.php
Normal file
96
src/Game/Service/PlayerService.php
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Game\Service;
|
||||||
|
|
||||||
|
use App\Game\Entity\Game;
|
||||||
|
use App\Game\Entity\Player;
|
||||||
|
use App\Game\Entity\SessionSetting;
|
||||||
|
use App\Game\Enum\SessionSettingType;
|
||||||
|
use App\Game\Enum\SessionStatus;
|
||||||
|
use App\Game\Repository\PlayerRepository;
|
||||||
|
use App\Game\Repository\SessionSettingRepository;
|
||||||
|
use App\Tech\Entity\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
|
class PlayerService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private PlayerRepository $playerRepository,
|
||||||
|
private SessionSettingRepository $sessionSettingRepository,
|
||||||
|
private EntityManagerInterface $entityManager,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetCurrentlyActiveAsPlayer(User $user): ?Player
|
||||||
|
{
|
||||||
|
$player = $this->playerRepository->createQueryBuilder('p')
|
||||||
|
->join('p.session', 's')
|
||||||
|
->where('p.user = :user')
|
||||||
|
->andWhere('s.status IN (:statuses)')
|
||||||
|
->setParameter('user', $user)
|
||||||
|
->setParameter('statuses', [
|
||||||
|
SessionStatus::READY,
|
||||||
|
SessionStatus::PLAYING,
|
||||||
|
])
|
||||||
|
->setMaxResults(1)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
|
||||||
|
return $player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentPwdOfPlayer(Player $player) : ?string {
|
||||||
|
$settingName = SessionSettingType::tryFrom('PwdForPlayer' . $player->getScreen());
|
||||||
|
|
||||||
|
if (!$settingName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting = $this->sessionSettingRepository->getSetting($player->getSession(), $settingName, $player);
|
||||||
|
return $setting?->getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveCurrentPwdOfPlayer(Player $player, string $newLocation)
|
||||||
|
{
|
||||||
|
$settingName = SessionSettingType::tryFrom('PwdForPlayer' . $player->getScreen());
|
||||||
|
|
||||||
|
if (!$settingName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting = $this->sessionSettingRepository->getSetting($player->getSession(), $settingName, $player);
|
||||||
|
$setting->setValue($newLocation);
|
||||||
|
|
||||||
|
$this->entityManager->persist($setting);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDeletedFilesOfSession(Player $player): array
|
||||||
|
{
|
||||||
|
$setting = $this->sessionSettingRepository->getSetting($player->getSession(), SessionSettingType::SET_OF_DELETED_FILES);
|
||||||
|
if (!$setting || !$setting->getValue()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_decode($setting->getValue(), true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addDeletedFileToSession(Player $player, string $filename): void
|
||||||
|
{
|
||||||
|
$setting = $this->sessionSettingRepository->getSetting($player->getSession(), SessionSettingType::SET_OF_DELETED_FILES);
|
||||||
|
if (!$setting) {
|
||||||
|
$setting = new SessionSetting();
|
||||||
|
$setting->setSession($player->getSession());
|
||||||
|
$setting->setName(SessionSettingType::SET_OF_DELETED_FILES);
|
||||||
|
}
|
||||||
|
|
||||||
|
$deletedFiles = json_decode($setting->getValue() ?? '[]', true) ?? [];
|
||||||
|
if (!in_array($filename, $deletedFiles)) {
|
||||||
|
$deletedFiles[] = $filename;
|
||||||
|
$setting->setValue(json_encode($deletedFiles));
|
||||||
|
|
||||||
|
$this->entityManager->persist($setting);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ class ActivationController extends AbstractController
|
|||||||
try {
|
try {
|
||||||
$emailVerifier->handleEmailConfirmation($request, $user);
|
$emailVerifier->handleEmailConfirmation($request, $user);
|
||||||
} catch (VerifyEmailExceptionInterface $exception) {
|
} 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');
|
return $this->redirectToRoute('app_register');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace App\Tech\Controller;
|
|||||||
|
|
||||||
use App\Tech\Entity\User;
|
use App\Tech\Entity\User;
|
||||||
use App\Tech\Form\RegistrationFormType;
|
use App\Tech\Form\RegistrationFormType;
|
||||||
|
use App\Tech\Form\ResendVerificationEmailFormType;
|
||||||
|
use App\Tech\Repository\UserRepository;
|
||||||
use App\Tech\Service\EmailVerifier;
|
use App\Tech\Service\EmailVerifier;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
@@ -31,13 +33,15 @@ class RegistrationController extends AbstractController
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$user->setRoles(['ROLE_USER', 'ROLE_PLAYER']);
|
||||||
|
|
||||||
$entityManager->persist($user);
|
$entityManager->persist($user);
|
||||||
$entityManager->flush();
|
$entityManager->flush();
|
||||||
|
|
||||||
// generate a signed url and email it to the user
|
// generate a signed url and email it to the user
|
||||||
$emailVerifier->sendEmailConfirmation('app_verify_email', $user,
|
$emailVerifier->sendEmailConfirmation('app_verify_email', $user,
|
||||||
(new TemplatedEmail())
|
(new TemplatedEmail())
|
||||||
->from('noreply@escapepage.dev')
|
->from($this->getParameter('mailer_from'))
|
||||||
->to($user->getEmail())
|
->to($user->getEmail())
|
||||||
->subject('Please Confirm your Email')
|
->subject('Please Confirm your Email')
|
||||||
->htmlTemplate('tech/registration/confirmation_email.html.twig')
|
->htmlTemplate('tech/registration/confirmation_email.html.twig')
|
||||||
@@ -52,4 +56,41 @@ class RegistrationController extends AbstractController
|
|||||||
'registrationForm' => $form->createView(),
|
'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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,15 +26,13 @@ class ChangePasswordFormType extends AbstractType
|
|||||||
],
|
],
|
||||||
'first_options' => [
|
'first_options' => [
|
||||||
'constraints' => [
|
'constraints' => [
|
||||||
new NotBlank([
|
new NotBlank(message: 'Please enter a password'),
|
||||||
'message' => 'Please enter a password',
|
new Length(
|
||||||
]),
|
min: 12,
|
||||||
new Length([
|
minMessage: 'Your password should be at least {{ limit }} characters',
|
||||||
'min' => 12,
|
|
||||||
'minMessage' => 'Your password should be at least {{ limit }} characters',
|
|
||||||
// max length allowed by Symfony for security reasons
|
// max length allowed by Symfony for security reasons
|
||||||
'max' => 4096,
|
max: 4096,
|
||||||
]),
|
),
|
||||||
new PasswordStrength(),
|
new PasswordStrength(),
|
||||||
new NotCompromisedPassword(),
|
new NotCompromisedPassword(),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
namespace App\Tech\Form;
|
namespace App\Tech\Form;
|
||||||
|
|
||||||
use App\Tech\Entity\User;
|
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\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
@@ -21,9 +23,7 @@ class RegistrationFormType extends AbstractType
|
|||||||
->add('email', EmailType::class)
|
->add('email', EmailType::class)
|
||||||
->add('username', TextType::class, [
|
->add('username', TextType::class, [
|
||||||
'constraints' => [
|
'constraints' => [
|
||||||
new NotBlank([
|
new NotBlank(message: 'Please enter a username'),
|
||||||
'message' => 'Please enter a username',
|
|
||||||
]),
|
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
->add('plainPassword', RepeatedType::class, [
|
->add('plainPassword', RepeatedType::class, [
|
||||||
@@ -33,17 +33,19 @@ class RegistrationFormType extends AbstractType
|
|||||||
'first_options' => ['label' => 'Password'],
|
'first_options' => ['label' => 'Password'],
|
||||||
'second_options' => ['label' => 'Repeat Password'],
|
'second_options' => ['label' => 'Repeat Password'],
|
||||||
'constraints' => [
|
'constraints' => [
|
||||||
new NotBlank([
|
new NotBlank(message: 'Please enter a password'),
|
||||||
'message' => 'Please enter a password',
|
new Length(
|
||||||
]),
|
min: 6,
|
||||||
new Length([
|
minMessage: 'Your password should be at least {{ limit }} characters',
|
||||||
'min' => 6,
|
|
||||||
'minMessage' => 'Your password should be at least {{ limit }} characters',
|
|
||||||
// max length allowed by Symfony for security reasons
|
// max length allowed by Symfony for security reasons
|
||||||
'max' => 4096,
|
max: 4096,
|
||||||
]),
|
),
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
|
->add('captcha', Recaptcha3Type::class, [
|
||||||
|
'constraints' => new Recaptcha3(),
|
||||||
|
'action_name' => 'registration',
|
||||||
|
])
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
34
src/Tech/Form/ResendVerificationEmailFormType.php
Normal file
34
src/Tech/Form/ResendVerificationEmailFormType.php
Normal 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([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,9 +16,7 @@ class ResetPasswordRequestFormType extends AbstractType
|
|||||||
->add('email', EmailType::class, [
|
->add('email', EmailType::class, [
|
||||||
'attr' => ['autocomplete' => 'email'],
|
'attr' => ['autocomplete' => 'email'],
|
||||||
'constraints' => [
|
'constraints' => [
|
||||||
new NotBlank([
|
new NotBlank(message: 'Please enter your email'),
|
||||||
'message' => 'Please enter your email',
|
|
||||||
]),
|
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
;
|
;
|
||||||
|
|||||||
22
src/Tech/Message/ProcessTaskMessage.php
Normal file
22
src/Tech/Message/ProcessTaskMessage.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tech\Message;
|
||||||
|
|
||||||
|
class ProcessTaskMessage
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private string $taskName,
|
||||||
|
private array $payload = [],
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTaskName(): string
|
||||||
|
{
|
||||||
|
return $this->taskName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPayload(): array
|
||||||
|
{
|
||||||
|
return $this->payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/Tech/MessageHandler/ProcessTaskMessageHandler.php
Normal file
25
src/Tech/MessageHandler/ProcessTaskMessageHandler.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tech\MessageHandler;
|
||||||
|
|
||||||
|
use App\Tech\Message\ProcessTaskMessage;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||||
|
|
||||||
|
#[AsMessageHandler]
|
||||||
|
class ProcessTaskMessageHandler
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(ProcessTaskMessage $message): void
|
||||||
|
{
|
||||||
|
$this->logger->info('Processing task: ' . $message->getTaskName(), [
|
||||||
|
'payload' => $message->getPayload(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Implement logic based on taskName here
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,4 +32,16 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
|
|||||||
$this->getEntityManager()->persist($user);
|
$this->getEntityManager()->persist($user);
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return User[]
|
||||||
|
*/
|
||||||
|
public function findByRole(string $role): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('u')
|
||||||
|
->andWhere('u.roles LIKE :role')
|
||||||
|
->setParameter('role', '%"' . $role . '"%')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user