This commit is contained in:
Serreau Jovann
2026-04-01 15:42:52 +02:00
parent beb12d2b75
commit 686de99909
258 changed files with 32993 additions and 2714 deletions

79
.env
View File

@@ -16,33 +16,80 @@
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=
APP_SECRET=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
APP_SHARE_DIR=var/share
###< symfony/framework-bundle ###
###> symfony/routing ###
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
DEFAULT_URI=http://localhost
DEFAULT_URI=https://esyweb.local
###< symfony/routing ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
DATABASE_URL="postgresql://app:secret@pgbouncer:6432/e_ticket?serverVersion=16&charset=utf8"
###< doctrine/doctrine-bundle ###
###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
MESSENGER_TRANSPORT_DSN=redis://redis:6379/messages
###< symfony/messenger ###
###> session ###
SESSION_HANDLER_DSN=redis://redis:6379/1
###< session ###
###> cache ###
REDIS_CACHE_DSN=redis://redis:6379/2
###< cache ###
###> symfony/mailer ###
MAILER_DSN=null://null
MAILER_DSN=smtp://mailpit:1025
###< symfony/mailer ###
###> vite ###
# 0 = dev (HMR via localhost:5173), 1 = prod (manifest build)
VITE_LOAD=0
REAL_MAIL=0
###< vite ###
STRIPE_PK=
STRIPE_SK=
STRIPE_WEBHOOK_SECRET=
STRIPE_WEBHOOK_SECRET_CONNECT=
STRIPE_MODE=test
STRIPE_FEE_RATE=0.015
STRIPE_FEE_FIXED=25
ADMIN_EMAIL=contact@e-cosplay.fr
###> SonarQube ###
SONARQUBE_URL=https://sn.esy-web.dev
SONARQUBE_BADGE_TOKEN=
SONARQUBE_PROJECT_KEY=crm-ecosplay
###< SonarQube ###
###> SSO E-Cosplay (Keycloak OIDC) ###
OAUTH_KEYCLOAK_CLIENT_ID=e-ticket
OAUTH_KEYCLOAK_CLIENT_SECRET=changeme
OAUTH_KEYCLOAK_URL=https://auth.esy-web.dev
OAUTH_KEYCLOAK_REALM=e-cosplay
###< SSO E-Cosplay (Keycloak OIDC) ###
###> Keycloak Admin Service Account ###
KEYCLOAK_ADMIN_CLIENT_ID=crm-ecosplay-admin
KEYCLOAK_ADMIN_CLIENT_SECRET=
###< Keycloak Admin Service Account ###
###> symfony/amazon-mailer ###
# MAILER_DSN=ses://ACCESS_KEY:SECRET_KEY@default?region=eu-west-1
# MAILER_DSN=ses+smtp://ACCESS_KEY:SECRET_KEY@default?region=eu-west-1
###< symfony/amazon-mailer ###
###> stripe/stripe-php ###
STRIPE_SECRET_KEY=sk_test_***
###< stripe/stripe-php ###
SMIME_PASSPHRASE=EVz5zNV8h4ndSLOCWO9JeaQnIertQm7k
SECRET_ANALYTICS=
###> docuseal ###
DOCUSEAL_URL=https://signature.esy-web.dev
DOCUSEAL_API=
DOCUSEAL_WEBHOOKS_SECRET_HEADER=X-Sign
DOCUSEAL_WEBHOOKS_SECRET=
###< docuseal ###

0
.gitea/.gitkeep Normal file
View File

1
.gitea/CODEOWNERS Normal file
View File

@@ -0,0 +1 @@
* @jovann @e-cosplay

353
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,353 @@
j'ai gitea + sonarqube auto hébger tu me conseiler quoi en plus pour analyser le code php
13:35
Bonne question ! Avec Gitea + SonarQube déjà en place, voici ce que je te recommanderais pour compléter ta chaîne d'analyse PHP :
Analyse statique complémentaire — Tu utilises déjà PHPStan via SonarQube, mais le lancer aussi en standalone (niveau max) te donne un feedback plus rapide dans ta CI. Psalm est une alternative/complément intéressant, surtout pour l'analyse de type et la détection de dead code. Les deux peuvent coexister.
Style & standards — PHP CS Fixer ou PHP_CodeSniffer pour enforcer un coding standard (PSR-12 ou custom). PHP CS Fixer s'intègre bien dans un pre-commit hook ou un job CI Gitea Actions/Drone.
Sécurité — Snyk ou Trivy pour scanner les dépendances Composer (CVE). Trivy est self-hostable et peut aussi scanner tes images Docker, ce qui colle bien avec ton stack. Semgrep est aussi un bon choix pour des règles de sécurité custom sur le code PHP (OWASP patterns, injections SQL, XSS).
Complexité & dette technique — PHPMetrics génère des rapports HTML visuels sur la complexité cyclomatique, le couplage, la maintenabilité. Ça complète bien SonarQube avec une vue plus "architecturale". dePHPend analyse les dépendances entre classes/packages pour détecter les couplages problématiques.
Tests & couverture — Si pas déjà en place, PHPUnit + Infection (mutation testing) pour mesurer la qualité réelle de tes tests, pas juste le coverage brut. SonarQube peut ingérer les rapports de coverage PHPUnit.
En priorité pour ton contexte (multi-tenant, ~85 sites ESY-WEB), je partirais sur :
PHPStan niveau max en CI (si pas déjà fait en standalone)
Trivy (scan dépendances Composer + images Docker — tu as déjà le stack Docker)
PHP CS Fixer en pre-commit
PHPMetrics pour le reporting visuel
Tout ça s'orchestre bien avec des Gitea Actions ou un pipeline Makefile, ce qui colle avec ton workflow habituel. Tu veux que je t'aide à mettre en place un de ces outils en particulier ?
name: CI on: push: branches: - master - main pull_request: jobs: sonarqube: runs_on: ubuntu-latest services: database: image: postgres:16-alpine env: POSTGRES_USER: app POSTGRES_PASSWORD: secret POS
pasted
j'ai deja tout sa
13:36
Ah ok, t'es déjà bien équipé ! Ton pipeline est solide. Voici ce qui manque et qui aurait une vraie valeur ajoutée :
Sécurité applicative — Semgrep pour de l'analyse SAST orientée patterns PHP (injections SQL, XSS, SSRF, deserialize unsafe). OWASP Dependency-Check couvre les dépendances, mais pas le code lui-même. Semgrep est gratuit en CLI et s'ajoute en une step.
Qualité / dette — PHPMetrics pour un rapport HTML visuel (complexité cyclomatique, couplage, maintenabilité par classe). SonarQube donne des métriques mais PHPMetrics a une vue "bubble chart" par classe qui aide à prioriser le refacto. Très rapide à ajouter, pas besoin de services.
Tests — Infection (mutation testing). Tu as déjà PHPUnit + coverage, mais le coverage seul ne dit pas si tes tests détectent vraiment les régressions. Infection mute ton code et vérifie que les tests cassent. Ça s'intègre bien avec le coverage.xml que tu génères déjà.
Infra/Docker — Trivy en remplacement ou complément de Hadolint. Hadolint lint les Dockerfiles, Trivy scanne les images buildées (CVE dans les packages OS + dépendances). Vu ton stack multi-container c'est pertinent.
Frontend — Lighthouse CI si tu as des pages publiques côté E-Cosplay. Perf, accessibilité, SEO en une step.
En résumé les ajouts concrets :
yaml
- name: Semgrep SAST
run: |
pip install semgrep --break-system-packages
semgrep scan --config=auto --json -o semgrep-report.json src/ || true
- name: PHPMetrics
run: |
composer global require phpmetrics/phpmetrics --no-interaction
phpmetrics --report-html=phpmetrics-report src/
- name: Infection (mutation testing)
run: |
vendor/bin/infection --threads=4 --min-msi=50 --min-covered-msi=60 --logger-json=infection-report.json || true
Semgrep est le plus impactant vu que t'as zéro SAST sur le code source actuellement — OWASP DC ne couvre que les dépendances. Tu veux que je t'intègre ça proprement dans ton workflow complet ?
name: CI on: push: branches: - master - main pull_request: jobs: sonarqube: runs_on: ubuntu-latest services: database: image: postgres:16-alpine env: POSTGRES_USER: app POSTGRES_PASSWORD: secret POS
pasted
13:37
Nickel, t'as intégré les trois. Ton pipeline est maintenant très complet. Quelques remarques d'optimisation :
Problème potentiel avec Infection — il a besoin du coverage.xml généré par PHPUnit, mais il faut lui passer explicitement. Vérifie que ton infection.json5 pointe bien dessus, sinon ajoute --coverage=coverage.xml ou mieux, utilise le flag --skip-initial-tests si tu veux qu'il réutilise le coverage existant.
PHPMetrics — composer global require peut être lent en CI. Une alternative plus rapide :
yaml
- name: PHPMetrics
run: |
wget -qO phpmetrics.phar https://github.com/phpmetrics/PhpMetrics/releases/latest/download/phpmetrics.phar
php phpmetrics.phar --report-html=phpmetrics-report src/
Semgrep — pip install en CI c'est aussi un peu lent. L'image Docker officielle est plus rapide :
yaml
- name: Semgrep SAST
run: |
docker run --rm -v "${GITHUB_WORKSPACE}:/src" semgrep/semgrep semgrep scan --config=auto --json -o /src/semgrep-report.json /src/src/ || true
Mais vu que t'es sur Gitea Actions (pas GitHub), vérifie que le Docker-in-Docker fonctionne dans ton runner. Sinon le pip install reste le plus safe.
Ce qui manque pour boucler — tous ces rapports (semgrep, phpmetrics, infection, hadolint) sont générés mais pas exploités. Deux options :
SonarQube — configure sonar.externalIssuesReportPaths dans ton sonar-project.properties pour ingérer les JSON de Semgrep et Hadolint. SonarQube les affichera comme "external issues".
Artifacts — upload les rapports comme artifacts de build pour les consulter après :
yaml
- name: Upload reports
uses: actions/upload-artifact@v3
with:
name: ci-reports
path: |
semgrep-report.json
phpmetrics-report/
infection-report.json
hadolint-*.json
dependency-check-report.*
eslint-report.json
phpstan-report.json
Ça te permettrait de consulter le rapport PHPMetrics HTML directement. Tu veux que je te génère le sonar-project.properties adapté avec l'import des rapports externes ?
name: CI on: push: branches: - master - main pull_request: jobs: sonarqube: runs_on: ubuntu-latest services: database: image: postgres:16-alpine env: POSTGRES_USER: app POSTGRES_PASSWORD: secret POS
pasted
13:38
OK, quelques corrections à faire sur ton workflow actuel :
PHPMetrics — tu mélanges les deux approches. Tu fais composer global require mais ensuite php phpmetrics.phar (qui n'existe pas puisque t'as pas téléchargé le phar). Choisis l'un ou l'autre :
yaml
# Option A : phar (plus rapide)
- name: PHPMetrics
run: |
wget -qO phpmetrics.phar https://github.com/phpmetrics/PhpMetrics/releases/latest/download/phpmetrics.phar
php phpmetrics.phar --report-html=phpmetrics-report src/
# Option B : composer global
- name: PHPMetrics
run: |
composer global require phpmetrics/phpmetrics --no-interaction
~/.composer/vendor/bin/phpmetrics --report-html=phpmetrics-report src/
Semgrep Docker — attention, sur Gitea Actions le ${GITHUB_WORKSPACE} n'existe probablement pas. Selon ton runner Gitea, ça sera plutôt ${GITEA_WORKSPACE} ou ${{ github.workspace }} (si ton runner est compatible). Vérifie, sinon reviens au pip install qui est plus portable :
yaml
- name: Semgrep SAST
run: |
pip install semgrep --break-system-packages
semgrep scan --config=auto --json -o semgrep-report.json src/ || true
Upload des artifacts + sonar-project.properties — tu veux que je te génère les deux fichiers ? Le sonar-project.properties avec l'import de tous tes rapports externes (Semgrep, PHPStan, ESLint, Hadolint, coverage), et la step d'upload artifacts à ajouter en fin de pipeline ?
sort tout le workflow stp
13:38
Voici le workflow complet corrigé :
Presenting files
Presenting files
Workflow CI complet corrigé avec tous les outils d'analyse
ci.yml
sonar-project.properties avec import de tous les rapports externes
sonar-project.properties
Presenting file(s)...
Claude est une IA et peut faire des erreurs. Veuillez vérifier les réponses.
Ci · YML
Copier
name: CI
on:
push:
branches:
- master
- main
pull_request:
jobs:
ci:
runs_on: ubuntu-latest
services:
database:
image: postgres:16-alpine
env:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: crm_ecosplay
options: >-
--health-cmd "pg_isready -U app -d crm_ecosplay"
--health-interval 5s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 5
meilisearch:
image: getmeili/meilisearch:latest
env:
MEILI_MASTER_KEY: test
MEILI_ENV: development
env:
DATABASE_URL: "postgresql://app:secret@database:5432/crm_ecosplay?serverVersion=16&charset=utf8"
MESSENGER_TRANSPORT_DSN: "redis://redis:6379/messages"
MAILER_DSN: "null://null"
MEILISEARCH_URL: "http://meilisearch:7700"
MEILISEARCH_API_KEY: "test"
APP_ENV: test
steps:
# ─── Setup ───────────────────────────────────────────────
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: intl, pdo_pgsql, zip, gd, redis, imagick
coverage: xdebug
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
- name: Setup Bun
uses: oven-sh/setup-bun@v1
# ─── Dependencies ────────────────────────────────────────
- name: Install PHP dependencies
run: composer install --no-interaction --prefer-dist
- name: Install JS dependencies
run: |
bun install
npm install
# ─── Security audits ─────────────────────────────────────
- name: Composer security audit
run: composer audit
# ─── Linting ─────────────────────────────────────────────
- name: Lint Twig
run: php bin/console lint:twig templates/
- name: Lint YAML
run: php bin/console lint:yaml config/
- name: Lint Container
run: php bin/console lint:container
- name: PHP CS Fixer
run: vendor/bin/php-cs-fixer fix --dry-run --diff
- name: ESLint report
run: bunx eslint assets/ --ext .js,.ts -f json -o eslint-report.json || true
- name: Stylelint
run: bunx stylelint "assets/**/*.scss"
# ─── Static analysis ─────────────────────────────────────
- name: PHPStan report
run: vendor/bin/phpstan analyse src/ --level=6 --no-progress --error-format=json > phpstan-report.json || true
- name: Semgrep SAST
run: |
pip install semgrep --break-system-packages
semgrep scan --config=auto --json -o semgrep-report.json src/ || true
# ─── Build ───────────────────────────────────────────────
- name: Build assets
run: bun run build
# ─── Tests ───────────────────────────────────────────────
- name: JS tests with coverage
run: bun run test:coverage
- name: Create test database
run: php bin/console doctrine:database:create --env=test --if-not-exists
- name: Create database schema
run: php bin/console doctrine:schema:create --env=test
- name: PHPUnit with coverage
run: vendor/bin/phpunit --testdox --coverage-clover coverage.xml --coverage-text
- name: Infection (mutation testing)
run: |
vendor/bin/infection \
--threads=4 \
--skip-initial-tests \
--coverage=. \
--min-msi=50 \
--min-covered-msi=60 \
--logger-json=infection-report.json || true
# ─── Metrics ─────────────────────────────────────────────
- name: PHPMetrics
run: |
wget -qO phpmetrics.phar https://github.com/phpmetrics/PhpMetrics/releases/latest/download/phpmetrics.phar
php phpmetrics.phar --report-html=phpmetrics-report src/
# ─── Docker linting ──────────────────────────────────────
- name: Hadolint
run: |
wget -qO hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
chmod +x hadolint
./hadolint docker/php/dev/Dockerfile -f json > hadolint-dev.json || true
./hadolint docker/php/prod/Dockerfile -f json > hadolint-prod.json || true
# ─── Dependency vulnerability scan ───────────────────────
- name: OWASP Dependency-Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'crm-ecosplay'
path: '.'
format: 'JSON,HTML'
args: >
--scan composer.lock
--scan package.json
--out .
--disableAssembly
--nvdApiKey ${{ secrets.NVD_API_KEY }}
continue-on-error: true
- name: Rename Dependency-Check reports
run: |
mv dependency-check-report.json dc-report.json 2>/dev/null || true
mv dependency-check-report.html dc-report.html 2>/dev/null || true
# ─── SonarQube ───────────────────────────────────────────
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@v5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: https://sn.esy-web.dev
# ─── Upload reports ──────────────────────────────────────
- name: Upload CI reports
uses: actions/upload-artifact@v3
if: always()
with:
name: ci-reports
retention-days: 30
path: |
coverage.xml
phpstan-report.json
eslint-report.json
semgrep-report.json
infection-report.json
hadolint-dev.json
hadolint-prod.json
dc-report.*
phpmetrics-report/

View File

@@ -0,0 +1,20 @@
name: Deploy to production
on:
workflow_dispatch:
schedule:
- cron: '0 1,22 * * *'
jobs:
deploy:
runs_on: ubuntu-latest
steps:
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan 34.90.187.4 >> ~/.ssh/known_hosts
- name: Deploy
run: ssh bot@34.90.187.4 "cd /var/www/crm-ecosplay && ansible-playbook ansible/deploy.yml -i ansible/hosts.ini --vault-password-file <(echo '${{ secrets.ANSIBLE_VAULT_PASSWORD }}')"

View File

@@ -0,0 +1,116 @@
name: SonarQube Full Scan
on:
workflow_dispatch:
jobs:
scan:
runs_on: ubuntu-latest
services:
database:
image: postgres:16-alpine
env:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: crm_ecosplay
options: >-
--health-cmd "pg_isready -U app -d crm_ecosplay"
--health-interval 5s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 5
meilisearch:
image: getmeili/meilisearch:latest
env:
MEILI_MASTER_KEY: test
MEILI_ENV: development
env:
DATABASE_URL: "postgresql://app:secret@database:5432/crm_ecosplay?serverVersion=16&charset=utf8"
MESSENGER_TRANSPORT_DSN: "redis://redis:6379/messages"
MAILER_DSN: "null://null"
MEILISEARCH_URL: "http://meilisearch:7700"
MEILISEARCH_API_KEY: "test"
APP_ENV: test
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: intl, pdo_pgsql, zip, gd, redis, imagick
coverage: xdebug
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
- name: Setup Bun
uses: oven-sh/setup-bun@v1
- name: Install PHP dependencies
run: composer install --no-interaction --prefer-dist
- name: Install JS dependencies
run: bun install
- name: JS tests with coverage
run: bun run test:coverage
- name: PHPStan report
run: vendor/bin/phpstan analyse src/ --level=6 --no-progress --error-format=json > phpstan-report.json || true
- name: Build assets
run: bun run build
- name: Create test database
run: php bin/console doctrine:database:create --env=test --if-not-exists
- name: Create database schema
run: php bin/console doctrine:schema:create --env=test
- name: PHPUnit with coverage
run: vendor/bin/phpunit --coverage-clover coverage.xml
- name: ESLint report
run: bunx eslint assets/ --ext .js,.ts -f json -o eslint-report.json || true
- name: Hadolint
run: |
wget -qO hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
chmod +x hadolint
./hadolint docker/php/dev/Dockerfile -f json > hadolint-dev.json || true
./hadolint docker/php/prod/Dockerfile -f json > hadolint-prod.json || true
- name: OWASP Dependency-Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'crm-ecosplay'
path: '.'
format: 'JSON,HTML'
args: >
--scan composer.lock
--scan package.json
--out .
--disableAssembly
continue-on-error: true
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@v5
with:
args: >
-Dsonar.qualitygate.wait=true
-Dsonar.scm.forceReloadAll=true
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: https://sn.esy-web.dev

38
.gitignore vendored
View File

@@ -1,4 +1,3 @@
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
@@ -14,7 +13,44 @@
/.phpunit.cache/
###< phpunit/phpunit ###
/public/build/
/public/uploads/
/public/.update
docker-compose-prod.yml
docker/pgsql/pgbouncer.ini
docker/pgsql/userlist.txt
node_modules/
.idea/
/cert/
/config/cert/private-key.pem
/public/media/
/coverage/
/coverage-clover.xml
###> friendsofphp/php-cs-fixer ###
/.php-cs-fixer.php
/.php-cs-fixer.cache
###< friendsofphp/php-cs-fixer ###
###> symfony/asset-mapper ###
/public/assets/
/assets/vendor/
###< symfony/asset-mapper ###
###> spomky-labs/pwa-bundle ###
/public/sw.js
/public/workbox
/public/idb
/public/pwa
/public/favicon.ico
/public/site.webmanifest
/public/site.*.webmanifest
###< spomky-labs/pwa-bundle ###
###> liip/imagine-bundle ###
/public/media/cache/
###< liip/imagine-bundle ###
###> phpstan/phpstan ###
phpstan.neon
###< phpstan/phpstan ###

17
.php-cs-fixer.dist.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
$finder = (new PhpCsFixer\Finder())
->in(__DIR__)
->exclude('var')
->notPath([
'config/bundles.php',
'config/reference.php',
])
;
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
])
->setFinder($finder)
;

31
AGENT.md Normal file
View File

@@ -0,0 +1,31 @@
# AGENT.md
## Interdiction generale
**Aucun agent IA, assistant de code, ou outil automatise n'est autorise a modifier, committer, pousser, supprimer ou alterer de quelque maniere que ce soit les fichiers de ce depot.**
Cela inclut, sans s'y limiter :
- La modification de fichiers existants
- La creation de nouveaux fichiers
- La suppression de fichiers
- Les commits et pushs Git
- L'ouverture de pull requests ou d'issues
- La modification de la configuration du projet
- L'execution de commandes destructrices
## Portee
Cette interdiction s'applique a tous les agents IA et outils automatises, y compris mais sans s'y limiter :
- Claude Code / Claude
- GitHub Copilot
- Cursor
- Windsurf
- Devin
- OpenAI Codex
- Tout autre assistant IA ou bot
## Exceptions
Aucune exception. Toute intervention IA sur ce depot est strictement interdite sauf autorisation explicite et ecrite du proprietaire du projet.

229
Makefile Normal file
View File

@@ -0,0 +1,229 @@
.DEFAULT_GOAL := help
## —— Help ————————————————————————————————————————
help: ## Affiche la liste des commandes disponibles
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
## —— Docker ——————————————————————————————————————
build_dev: ## Build les images Docker pour le dev
docker compose -f docker-compose-dev.yml build
build_prod: ## Build les images Docker pour la prod
docker compose -f docker-compose-prod.yml build
start_dev: ## Lance les containers dev
docker compose -f docker-compose-dev.yml up
start_prod: ## Lance les containers prod en background
docker compose -f docker-compose-prod.yml up -d
stop_dev: ## Arrete les containers dev
docker compose -f docker-compose-dev.yml down
purge_dev: ## Arrete et purge les containers dev (volumes inclus)
docker compose -f docker-compose-dev.yml down -v
stop_prod: ## Arrete les containers prod
docker compose -f docker-compose-prod.yml down
## —— Git ——————————————————————————————————————————
pull_dev: ## Pull les images Docker dev
docker compose -f docker-compose-dev.yml pull
pull_prod: ## Pull les images Docker prod
docker compose -f docker-compose-prod.yml pull
## —— Ansible —————————————————————————————————————
deploy_prod: ## Deploy en prod via Ansible (demande le vault password)
ansible-playbook ansible/deploy.yml -i ansible/hosts.ini --ask-vault-pass
## —— Install ——————————————————————————————————————
install_dev: ## Install les dependances dev via Docker
docker compose -f docker-compose-dev.yml exec php composer install
docker compose -f docker-compose-dev.yml exec bun bun install
install_prod: ## Install les dependances et build les assets pour la prod
docker compose -f docker-compose-prod.yml exec php composer install --no-dev --optimize-autoloader
bun install
bun run build
## —— Tests ————————————————————————————————————————
test_db_create: ## Cree la base de donnees de test
docker compose -f docker-compose-dev.yml exec database psql -U app -d crm_ecosplay -tc "SELECT 1 FROM pg_database WHERE datname = 'crm_ecosplay_test'" | grep -q 1 || \
docker compose -f docker-compose-dev.yml exec database psql -U app -d crm_ecosplay -c "CREATE DATABASE crm_ecosplay_test"
test_db_schema: ## Cree/met a jour le schema de la base de test
docker compose -f docker-compose-dev.yml exec php php bin/console doctrine:schema:update --force --env=test
test_db_migrate: ## Execute les migrations sur la base de test
docker compose -f docker-compose-dev.yml exec php php bin/console doctrine:migrations:migrate --no-interaction --env=test
test_db_reset: ## Supprime et recree la base de test depuis zero
docker compose -f docker-compose-dev.yml exec database psql -U app -d crm_ecosplay -c "DROP DATABASE IF EXISTS crm_ecosplay_test"
docker compose -f docker-compose-dev.yml exec database psql -U app -d crm_ecosplay -c "CREATE DATABASE crm_ecosplay_test"
$(MAKE) test_db_schema
test_db_setup: ## Setup complet de la base de test (cree si besoin + schema)
$(MAKE) test_db_create
$(MAKE) test_db_schema
run_test: ## Lance les tests PHP et JS via Docker dev
docker compose -f docker-compose-dev.yml exec php php bin/phpunit
docker compose -f docker-compose-dev.yml exec bun bun run test
run_test_php: ## Lance uniquement les tests PHP via Docker dev
docker compose -f docker-compose-dev.yml exec php php bin/phpunit
run_test_js: ## Lance uniquement les tests JS via Docker dev
docker compose -f docker-compose-dev.yml exec bun bun run test
run_test_coverage_php: ## Lance les tests PHP avec couverture via Docker dev
docker compose -f docker-compose-dev.yml exec php php bin/phpunit --coverage-text --coverage-html=coverage/php
run_test_coverage_js: ## Lance les tests JS avec couverture via Docker dev
docker compose -f docker-compose-dev.yml exec bun bun run test -- --coverage
run_test_file: ## Lance un fichier de test PHP specifique (usage: make run_test_file FILE=tests/Service/AuditServiceTest.php)
docker compose -f docker-compose-dev.yml exec php php bin/phpunit $(FILE)
## —— PWA —————————————————————————————————————————
pwa_dev: ## Compile les assets PWA en dev via Docker
docker compose -f docker-compose-dev.yml exec php php bin/console pwa:compile
pwa_prod: ## Compile les assets PWA en prod via Docker
docker compose -f docker-compose-prod.yml exec php php bin/console pwa:compile --env=prod
## —— Traductions ——————————————————————————————————
trans: ## Traduit les fichiers YAML FR vers EN/ES/DE/IT via LibreTranslate (Docker dev)
docker compose -f docker-compose-dev.yml exec php php bin/console app:translate
docker compose -f docker-compose-dev.yml stop libretranslate
trans_prod: ## Traduit les fichiers YAML FR vers EN/ES/DE/IT via LibreTranslate (Docker prod)
docker compose -f docker-compose-prod.yml exec -T php php bin/console app:translate --env=prod
docker compose -f docker-compose-prod.yml stop libretranslate
## —— Symfony ——————————————————————————————————————
entity: ## Creer ou modifier une entite via Docker dev
docker compose -f docker-compose-dev.yml exec php php bin/console make:entity
## —— Database ——————————————————————————————————————
migration_dev: ## Genere une migration via Docker dev
docker compose -f docker-compose-dev.yml exec php php bin/console make:migration
migrate_dev: ## Execute les migrations via Docker dev
docker compose -f docker-compose-dev.yml exec php php bin/console doctrine:migrations:migrate --no-interaction
force_sql_dev: ## Force la mise a jour du schema de la base de donnees dev
docker compose -f docker-compose-dev.yml exec php php bin/console doctrine:schema:update --force
migrate_prod: ## Execute les migrations en prod via Docker
docker compose -f docker-compose-prod.yml exec php php bin/console doctrine:migrations:migrate --no-interaction --env=prod
## —— Cache ————————————————————————————————————————
clear_prod: ## Clear le cache Symfony et le pool opcache en prod via Docker
docker compose -f docker-compose-prod.yml exec php php bin/console cache:clear --env=prod
docker compose -f docker-compose-prod.yml exec php php bin/console cache:pool:clear --all --env=prod
purge_liip_dev: ## Purge le cache Liip Imagine en dev via Docker
docker compose -f docker-compose-dev.yml exec php php bin/console liip:imagine:cache:remove
purge_liip_prod: ## Purge le cache Liip Imagine en prod via Docker
docker compose -f docker-compose-prod.yml exec php php bin/console liip:imagine:cache:remove --env=prod
## —— Maintenance ——————————————————————————————————
maintenance_on: ## Active le mode maintenance
touch public/.update
maintenance_off: ## Desactive le mode maintenance
rm -f public/.update
## —— Qualite ──────────────────────────────────────
qa: lint phpstan cs_check semgrep ## Lance tous les checks qualite (lint + analyse + sécurité)
qa_full: qa test_coverage infection phpmetrics ## Lance la suite complète (qa + tests coverage + mutation + metrics)
lint: ## Lance tous les linters (Twig, YAML, Container, ESLint, Stylelint)
docker compose -f docker-compose-dev.yml exec php php bin/console lint:twig templates/
docker compose -f docker-compose-dev.yml exec php php bin/console lint:yaml config/
docker compose -f docker-compose-dev.yml exec php php bin/console lint:container
docker compose -f docker-compose-dev.yml exec bun bunx eslint assets/ --ext .js,.ts
docker compose -f docker-compose-dev.yml exec bun bunx stylelint "assets/**/*.scss"
phpstan: ## Lance PHPStan niveau 6
docker compose -f docker-compose-dev.yml exec php vendor/bin/phpstan analyse src/ --level=6 --memory-limit=512M
phpstan_report: ## Lance PHPStan et genere le rapport JSON pour SonarQube
docker compose -f docker-compose-dev.yml exec php sh -c 'mkdir -p var/reports && vendor/bin/phpstan analyse src/ --level=6 --memory-limit=512M --no-progress --error-format=json > var/reports/phpstan-report.json || true'
cs_check: ## Verifie le code style (dry-run)
docker compose -f docker-compose-dev.yml exec php vendor/bin/php-cs-fixer fix --dry-run --diff
cs_fix: ## Corrige automatiquement le code style
docker compose -f docker-compose-dev.yml exec php vendor/bin/php-cs-fixer fix
eslint: ## Lance ESLint sur les assets JS/TS
docker compose -f docker-compose-dev.yml exec bun bunx eslint assets/ --ext .js,.ts
eslint_fix: ## Corrige automatiquement les erreurs ESLint
docker compose -f docker-compose-dev.yml exec bun bunx eslint assets/ --ext .js,.ts --fix
eslint_report: ## Lance ESLint et genere le rapport JSON pour SonarQube
docker compose -f docker-compose-dev.yml exec bun sh -c 'mkdir -p var/reports && bunx eslint assets/ --ext .js,.ts -f json -o var/reports/eslint-report.json || true'
stylelint: ## Lance Stylelint sur les fichiers SCSS
docker compose -f docker-compose-dev.yml exec bun bunx stylelint "assets/**/*.scss"
stylelint_fix: ## Corrige automatiquement les erreurs Stylelint
docker compose -f docker-compose-dev.yml exec bun bunx stylelint "assets/**/*.scss" --fix
semgrep: ## Lance Semgrep SAST sur src/
docker compose -f docker-compose-dev.yml exec php sh -c 'pip install semgrep --break-system-packages -q && semgrep scan --config=auto src/'
semgrep_report:
mkdir -p var/reports
# 1. On garde ton scan Docker (il marche très bien)
docker run --rm \
--user $(shell id -u):$(shell id -g) \
-v "$(PWD):/src" \
returntocorp/semgrep \
semgrep scan --config=auto --json -o var/reports/semgrep-raw.json src/ || true
# 2. Conversion JQ simplifiée (on ne garde QUE la ligne, pas la colonne)
cat var/reports/semgrep-raw.json | jq '{issues: [.results[] | {engineId: "semgrep", ruleId: .check_id, severity: (if .extra.severity == "ERROR" then "CRITICAL" else "MAJOR" end), type: "VULNERABILITY", primaryLocation: {message: .extra.message, filePath: .path, textRange: {startLine: .start.line}}}]}' > var/reports/semgrep-report.json
test_coverage: ## Lance les tests PHP avec couverture (clover + HTML + JUnit)
docker compose -f docker-compose-dev.yml exec php sh -c 'mkdir -p var/reports && php bin/phpunit --testdox --log-junit var/reports/phpunit.xml --coverage-clover var/reports/coverage.xml --coverage-html var/reports/coverage-html --coverage-text'
infection: ## Lance Infection (mutation testing) sur les tests PHP
docker compose -f docker-compose-dev.yml exec php sh -c 'mkdir -p var/reports && vendor/bin/infection --threads=4 --min-msi=50 --min-covered-msi=60 --logger-json=var/reports/infection-report.json || true'
infection_full: test_coverage infection ## Lance les tests avec couverture puis Infection
phpmetrics: ## Genere le rapport PHPMetrics HTML
docker compose -f docker-compose-dev.yml exec php sh -c 'mkdir -p var/reports && curl -sLo /tmp/phpmetrics.phar https://github.com/phpmetrics/PhpMetrics/releases/latest/download/phpmetrics.phar && php /tmp/phpmetrics.phar --report-html=var/reports/phpmetrics-report src/'
hadolint: ## Lance Hadolint sur les Dockerfiles
docker run --rm -v "$(PWD):/app" -w /app hadolint/hadolint hadolint docker/php/dev/Dockerfile
docker run --rm -v "$(PWD):/app" -w /app hadolint/hadolint hadolint docker/php/prod/Dockerfile
hadolint_report: ## Lance Hadolint sur le Dockerfile prod et genere le rapport JSON
mkdir -p var/reports
# Use -i (interactive) and ensure no TTY is allocated to avoid escape characters
docker run --rm -v "$(PWD):/app" -w /app hadolint/hadolint \
hadolint docker/php/prod/Dockerfile -f json > var/reports/hadolint-raw.json || true
# Cleanup: Ensure the file isn't empty and contains valid JSON brackets
@if [ ! -s var/reports/hadolint-raw.json ]; then echo "[]" > var/reports/hadolint-raw.json; fi
# Optional: Strip potential Docker/System noise from the file
@sed -i 's/^[^{|[ ]*//g' var/reports/hadolint-raw.json
audit: ## Lance l'audit de securite Composer
docker compose -f docker-compose-dev.yml exec php composer audit
reports: phpstan_report eslint_report semgrep_report test_coverage hadolint_report phpmetrics ## Genere tous les rapports pour SonarQube
## —— SonarQube ————————————————————————————————————
sonar: reports ## Genere les rapports puis lance le scan SonarQube
docker run --rm -v "$(PWD):/usr/src" -e SONAR_HOST_URL=https://sn.esy-web.dev -e SONAR_TOKEN=$(shell grep SONAR_TOKEN .env.local 2>/dev/null | cut -d= -f2 || echo "") sonarsource/sonar-scanner-cli
sonar_quick: ## Lance le scan SonarQube sans regenerer les rapports
docker run --rm -v "$(PWD):/usr/src" -e SONAR_HOST_URL=https://sn.esy-web.dev -e SONAR_TOKEN=$(shell grep SONAR_TOKEN .env.local 2>/dev/null | cut -d= -f2 || echo "") sonarsource/sonar-scanner-cli

0
ansible/.gitkeep Normal file
View File

34
ansible/backup.sh.j2 Normal file
View File

@@ -0,0 +1,34 @@
#!/bin/bash
# CRM Ecosplay database backup script
# Runs every 30 minutes via cron
BACKUP_DIR="/var/backups/crm-ecosplay"
DATE=$(date +%Y%m%d_%H%M%S)
FILENAME="crm_ecosplay_${DATE}.sql.gz"
KEEP_DAYS=1
# Dump database via Docker
docker compose -f /var/www/crm-ecosplay/docker-compose-prod.yml exec -T db-master pg_dump -U {{ db_user | default('crm-ecosplay') }} {{ db_name | default('crm-ecosplay') }} | gzip > "${BACKUP_DIR}/${FILENAME}"
# Check if backup was created
if [ -f "${BACKUP_DIR}/${FILENAME}" ] && [ -s "${BACKUP_DIR}/${FILENAME}" ]; then
echo "[$(date)] DB Backup OK: ${FILENAME} ($(du -h "${BACKUP_DIR}/${FILENAME}" | cut -f1))"
else
echo "[$(date)] ERROR: DB Backup failed"
exit 1
fi
# Backup uploads
UPLOADS_FILENAME="uploads_${DATE}.tar.gz"
tar -czf "${BACKUP_DIR}/${UPLOADS_FILENAME}" -C /var/www/crm-ecosplay/public uploads 2>/dev/null
if [ -f "${BACKUP_DIR}/${UPLOADS_FILENAME}" ]; then
echo "[$(date)] Uploads Backup OK: ${UPLOADS_FILENAME} ($(du -h "${BACKUP_DIR}/${UPLOADS_FILENAME}" | cut -f1))"
else
echo "[$(date)] WARNING: Uploads backup failed"
fi
# Remove backups older than KEEP_DAYS days
find "${BACKUP_DIR}" -name "crm_ecosplay_*.sql.gz" -mtime +${KEEP_DAYS} -delete
find "${BACKUP_DIR}" -name "uploads_*.tar.gz" -mtime +${KEEP_DAYS} -delete
echo "[$(date)] Cleaned backups older than ${KEEP_DAYS} days"

53
ansible/caddy.j2 Normal file
View File

@@ -0,0 +1,53 @@
crm.e-cosplay.fr {
tls {
dns cloudflare cfat_rIHZqzCm9GKK3xVnQDNGfu6J91TseIDdTKeuWSFUdf6ccd31
}
@static path /logo.png /favicon.ico /favicon.png /marker.png /screen.png /manifest.json /site.webmanifest /sw.js /unavailable.html /workbox/* /idb/* /build/* /uploads/* /pwa/*
handle @static {
root * /var/www/crm-crm-ecosplay/public
file_server
}
@maintenance file /var/www/crm-ecosplay/public/.update
handle @maintenance {
root * /var/www/crm-ecosplay/public
rewrite * /maintenance.html
file_server {
status 503
}
}
handle {
root * /var/www/crm-ecosplay/public
php_fastcgi localhost:4568 localhost:4569 {
root /app/public
lb_policy round_robin
trusted_proxies private_ranges
dial_timeout 5s
read_timeout 30s
write_timeout 30s
}
file_server
try_files {path} /index.php?{query}
}
handle_errors {
@unavailable expression `{err.status_code} in [502, 503]`
handle @unavailable {
root * /var/www/crm-ecosplay/public
rewrite * /unavailable.html
file_server
}
}
encode gzip
header {
-Server
}
log {
output file /var/log/caddy/ticket.e-cosplay.fr.log
}
}

278
ansible/deploy.yml Normal file
View File

@@ -0,0 +1,278 @@
---
# --- Server deployment ---
- name: Deploy crm-ecosplay to production
hosts: production
become: true
vars_files:
- vault.yml
pre_tasks:
- name: Enable maintenance mode
command: make maintenance_on
args:
chdir: /var/www/crm-ecosplay
- name: Get Docker socket GID
stat:
path: /var/run/docker.sock
register: docker_sock
- name: Set docker_gid fact
set_fact:
docker_gid: "{{ docker_sock.stat.gid }}"
- name: Generate analytics secret
set_fact:
analytics_secret: "{{ lookup('password', '/dev/null chars=ascii_lowercase,digits length=32') }}"
tasks:
- name: Deploy .env.local
template:
src: env.local.j2
dest: /var/www/crm-ecosplay/.env.local
owner: bot
group: bot
mode: "0600"
- name: Ensure cert directory exists
file:
path: /var/www/crm-ecosplay/config/cert
state: directory
owner: bot
group: bot
mode: "0700"
- name: Deploy S/MIME private key
copy:
content: "{{ smime_private_key }}"
dest: /var/www/crm-ecosplay/config/cert/private-key.pem
owner: bot
group: bot
mode: "0600"
- name: Pull latest code
command: git pull origin master
args:
chdir: /var/www/crm-ecosplay
become_user: bot
- name: Deploy PgBouncer config
template:
src: pgbouncer.ini.j2
dest: /var/www/crm-ecosplay/docker/pgsql/pgbouncer.ini
owner: bot
group: bot
mode: "0644"
- name: Deploy PgBouncer userlist
template:
src: userlist.txt.j2
dest: /var/www/crm-ecosplay/docker/pgsql/userlist.txt
owner: bot
group: bot
mode: "0644"
- name: Deploy docker-compose-prod.yml
template:
src: docker-compose-prod.yml.j2
dest: /var/www/crm-ecosplay/docker-compose-prod.yml
owner: bot
group: bot
mode: "0600"
- name: Build Docker images
command: make build_prod
args:
chdir: /var/www/crm-ecosplay
- name: Pull Docker images
command: make pull_prod
args:
chdir: /var/www/crm-ecosplay
- name: Stop production containers
command: make stop_prod
args:
chdir: /var/www/crm-ecosplay
- name: Start production containers
command: make start_prod
args:
chdir: /var/www/crm-ecosplay
- name: Install PHP dependencies
command: composer install --no-dev --optimize-autoloader
args:
chdir: /var/www/crm-ecosplay
become_user: bot
- name: Install JS dependencies
command: bun install
args:
chdir: /var/www/crm-ecosplay
become_user: bot
- name: Build assets
command: bun run build
args:
chdir: /var/www/crm-ecosplay
become_user: bot
- name: Wait for database to be ready
shell: |
for i in $(seq 1 30); do
docker compose -f docker-compose-prod.yml exec -T php php -r "new PDO('pgsql:host=pgbouncer;port=6432;dbname=crm-ecosplay','crm-ecosplay','{{ db_password }}');" 2>/dev/null && exit 0
sleep 1
done
exit 1
args:
chdir: /var/www/crm-ecosplay
- name: Clear Symfony cache before migration
command: docker compose -f docker-compose-prod.yml exec -T php php bin/console cache:clear --env=prod
args:
chdir: /var/www/crm-ecosplay
- name: Clear Redis cache pool (Doctrine L2 + app cache)
command: docker compose -f docker-compose-prod.yml exec -T php php bin/console cache:pool:clear cache.app --env=prod
args:
chdir: /var/www/crm-ecosplay
- name: Run migrations
command: make migrate_prod
args:
chdir: /var/www/crm-ecosplay
- name: Clear cache after migration
command: make clear_prod
args:
chdir: /var/www/crm-ecosplay
- name: Compile PWA assets
command: make pwa_prod
args:
chdir: /var/www/crm-ecosplay
- name: Ensure uploads directories exist with correct permissions
file:
path: "/var/www/crm-ecosplay/public/uploads/{{ item }}"
state: directory
owner: "1000"
group: "1000"
mode: "0755"
recurse: true
loop:
- logos
- name: Ensure var/payouts directory exists
file:
path: /var/www/crm-ecosplay/var/payouts
state: directory
owner: "1000"
group: "1000"
mode: "0755"
- name: Ensure Caddy sites directory exists
file:
path: /etc/caddy/sites
state: directory
owner: root
group: root
mode: "0755"
- name: Deploy Caddy config
template:
src: caddy.j2
dest: /etc/caddy/sites/crm-ecosplay.conf
owner: root
group: root
mode: "0644"
notify: Reload Caddy
- name: Create backup directory
file:
path: /var/backups/crm-ecosplay
state: directory
owner: bot
group: bot
mode: "0750"
- name: Deploy backup script
template:
src: backup.sh.j2
dest: /var/backups/crm-ecosplay/backup.sh
owner: bot
group: bot
mode: "0750"
- name: Configure backup cron (every 30 minutes)
cron:
name: "crm-ecosplay database backup"
minute: "*/30"
job: "/var/backups/crm-ecosplay/backup.sh >> /var/log/crm-ecosplay-backup.log 2>&1"
user: bot
- name: Configure expire pending orders cron (every 5 minutes)
cron:
name: "crm-ecosplay expire pending orders"
minute: "*/5"
job: "docker compose -f /var/www/crm-ecosplay/docker-compose-prod.yml exec -T php php bin/console app:orders:expire-pending --env=prod >> /var/log/crm-ecosplay-expire-orders.log 2>&1"
user: bot
- name: Configure messenger monitor cron (every hour)
cron:
name: "crm-ecosplay messenger monitor"
minute: "0"
job: "docker compose -f /var/www/crm-ecosplay/docker-compose-prod.yml exec -T php php bin/console app:monitor:messenger --env=prod >> /var/log/crm-ecosplay-messenger.log 2>&1"
user: bot
- name: Configure Meilisearch consistency check cron (daily at 3am)
cron:
name: "crm-ecosplay meilisearch consistency"
minute: "0"
hour: "3"
job: "docker compose -f /var/www/crm-ecosplay/docker-compose-prod.yml exec -T php php bin/console app:meilisearch:check-consistency --fix --env=prod >> /var/log/crm-ecosplay-meilisearch.log 2>&1"
user: bot
- name: Configure Stripe sync cron (every 6 hours)
cron:
name: "crm-ecosplay stripe sync"
minute: "0"
hour: "*/6"
job: "docker compose -f /var/www/crm-ecosplay/docker-compose-prod.yml exec -T php php bin/console app:stripe:sync --env=prod >> /var/log/crm-ecosplay-stripe-sync.log 2>&1"
user: bot
- name: Configure infra snapshot cron (every 5 minutes)
cron:
name: "crm-ecosplay infra snapshot"
minute: "*/5"
job: "docker compose -f /var/www/crm-ecosplay/docker-compose-prod.yml exec -T php php bin/console app:infra:snapshot --env=prod >> /var/log/crm-ecosplay-infra.log 2>&1"
user: bot
- name: Configure attestations cleanup cron (daily at 4am)
cron:
name: "crm-ecosplay attestations clean"
minute: "0"
hour: "4"
job: "docker compose -f /var/www/crm-ecosplay/docker-compose-prod.yml exec -T php php bin/console app:attestations:clean --env=prod >> /var/log/crm-ecosplay-attestations.log 2>&1"
user: bot
- name: Configure services health check cron (every 15 minutes)
cron:
name: "crm-ecosplay services check"
minute: "*/15"
job: "docker compose -f /var/www/crm-ecosplay/docker-compose-prod.yml exec -T php php bin/console app:services:check --env=prod >> /var/log/crm-ecosplay-services.log 2>&1"
user: bot
post_tasks:
- name: Disable maintenance mode
command: make maintenance_off
args:
chdir: /var/www/crm-ecosplay
handlers:
- name: Reload Caddy
systemd:
name: caddy
state: reloaded

View File

@@ -0,0 +1,211 @@
name: crm-ecosplay
services:
php:
build:
context: ./docker/php/prod
dockerfile: Dockerfile
deploy:
replicas: 2
resources:
limits:
cpus: "1.5"
memory: 1G
reservations:
cpus: "0.5"
memory: 256M
restart: unless-stopped
group_add:
- "{{ docker_gid }}"
volumes:
- .:/app
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- "4568-4569:9000"
networks:
- crm_ecosplay
depends_on:
pgbouncer:
condition: service_healthy
redis:
condition: service_healthy
db-master:
image: postgres:16-alpine
restart: unless-stopped
deploy:
resources:
limits:
cpus: "2.0"
memory: 4G
reservations:
cpus: "0.5"
memory: 1G
environment:
POSTGRES_USER: crm-ecosplay
POSTGRES_PASSWORD: {{ db_password }}
POSTGRES_DB: crm-ecosplay
command:
- postgres
- -c
- wal_level=replica
- -c
- max_wal_senders=3
- -c
- wal_keep_size=64MB
- -c
- hot_standby=on
- -c
- shared_buffers=1GB
- -c
- effective_cache_size=3GB
- -c
- work_mem=16MB
volumes:
- db-master-data:/var/lib/postgresql/data
- ./docker/pgsql/init-master.sh:/docker-entrypoint-initdb.d/init-master.sh
networks:
- crm_ecosplay
healthcheck:
test: ["CMD-SHELL", "pg_isready -U crm-ecosplay -d crm-ecosplay"]
interval: 5s
timeout: 5s
retries: 5
db-slave:
image: postgres:16-alpine
restart: unless-stopped
deploy:
resources:
limits:
cpus: "1.5"
memory: 2G
reservations:
cpus: "0.25"
memory: 512M
environment:
POSTGRES_USER: crm-ecosplay
POSTGRES_PASSWORD: {{ db_password }}
POSTGRES_DB: crm-ecosplay
PGDATA: /var/lib/postgresql/data
volumes:
- db-slave-data:/var/lib/postgresql/data
- ./docker/pgsql/init-slave.sh:/init-slave.sh
entrypoint: ["/bin/bash", "/init-slave.sh"]
command: ["postgres"]
networks:
- crm_ecosplay
depends_on:
db-master:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "pg_isready -U crm-ecosplay -d crm-ecosplay"]
interval: 5s
timeout: 5s
retries: 5
pgbouncer:
image: edoburu/pgbouncer
restart: unless-stopped
deploy:
resources:
limits:
cpus: "0.5"
memory: 128M
reservations:
cpus: "0.1"
memory: 32M
volumes:
- ./docker/pgsql/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini:ro
- ./docker/pgsql/userlist.txt:/etc/pgbouncer/userlist.txt:ro
networks:
- crm_ecosplay
depends_on:
db-master:
condition: service_healthy
db-slave:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 6432 -U crm-ecosplay"]
interval: 5s
timeout: 5s
retries: 5
messenger:
build:
context: ./docker/php/prod
dockerfile: Dockerfile
command: php bin/console messenger:consume async --time-limit=3600 --memory-limit=256M --limit=500 -vv
deploy:
replicas: 2
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
restart: unless-stopped
volumes:
- .:/app
networks:
- crm_ecosplay
depends_on:
pgbouncer:
condition: service_healthy
redis:
condition: service_healthy
meilisearch:
condition: service_started
redis:
image: redis:7-alpine
restart: unless-stopped
deploy:
resources:
limits:
cpus: "1.0"
memory: 1G
reservations:
cpus: "0.25"
memory: 128M
command: redis-server --requirepass {{ redis_password }} --maxmemory 768mb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
networks:
- crm_ecosplay
healthcheck:
test: ["CMD", "redis-cli", "-a", "{{ redis_password }}", "ping"]
interval: 5s
timeout: 5s
retries: 5
meilisearch:
image: getmeili/meilisearch:latest
restart: unless-stopped
deploy:
resources:
limits:
cpus: "1.0"
memory: 1G
reservations:
cpus: "0.25"
memory: 256M
environment:
MEILI_MASTER_KEY: {{ meilisearch_master_key }}
MEILI_ENV: production
MEILI_NO_ANALYTICS: true
volumes:
- meilisearch-data:/meili_data
networks:
- crm_ecosplay
networks:
crm_ecosplay:
driver: bridge
volumes:
db-master-data:
db-slave-data:
redis-data:
meilisearch-data:

33
ansible/env.local.j2 Normal file
View File

@@ -0,0 +1,33 @@
APP_ENV=prod
APP_SECRET={{ app_secret }}
DATABASE_URL="postgresql://crm-ecosplay:{{ db_password }}@pgbouncer:6432/crm-ecosplay?serverVersion=16&charset=utf8"
MESSENGER_TRANSPORT_DSN=redis://:{{ redis_password }}@redis:6379/messages
SESSION_HANDLER_DSN=redis://:{{ redis_password }}@redis:6379/1
REDIS_CACHE_DSN=redis://:{{ redis_password }}@redis:6379/2
MAILER_DSN={{ mailer_dsn }}
DEFAULT_URI=https://ticket.e-cosplay.fr
VITE_LOAD=1
REAL_MAIL=1
OUTSIDE_URL=https://ticket.e-cosplay.fr
STRIPE_PK={{ stripe_pk }}
STRIPE_SK={{ stripe_sk }}
STRIPE_WEBHOOK_SECRET={{ stripe_webhook_secret }}
STRIPE_WEBHOOK_SECRET_CONNECT={{ stripe_webhook_secret_connect }}
STRIPE_MODE=live
SMIME_PASSPHRASE='{{ smime_passphrase }}'
MEILISEARCH_URL=http://meilisearch:7700
MEILISEARCH_API_KEY={{ meilisearch_api_key }}
SONARQUBE_URL=https://sn.esy-web.dev
SONARQUBE_BADGE_TOKEN={{ sonarqube_badge_token }}
SONARQUBE_PROJECT_KEY=crm-ecosplay
OAUTH_KEYCLOAK_CLIENT_ID=crm_ecosplay
OAUTH_KEYCLOAK_CLIENT_SECRET=QiksEpHqDCHFPMM9CWb3RHfag31VJfIV
OAUTH_KEYCLOAK_URL=https://auth.esy-web.dev
OAUTH_KEYCLOAK_REALM=e-cosplay
SECRET_ANALYTICS={{ analytics_secret }}
KEYCLOAK_ADMIN_CLIENT_ID=crm-ecosplay-admin
KEYCLOAK_ADMIN_CLIENT_SECRET={{ keycloak_admin_client_secret }}
DOCUSEAL_URL=https://signature.esy-web.dev
DOCUSEAL_API={{ docuseal_api }}
DOCUSEAL_WEBHOOKS_SECRET_HEADER=X-Sign
DOCUSEAL_WEBHOOKS_SECRET={{ docuseal_webhooks_secret }}

5
ansible/hosts.ini Normal file
View File

@@ -0,0 +1,5 @@
[production]
127.0.0.1 ansible_user=bot ansible_become=yes ansible_become_method=sudo ansible_connection=local
[production:vars]
deploy_path=/var/www/crm.ecosplay/

12
ansible/messenger.j2 Normal file
View File

@@ -0,0 +1,12 @@
[program:crm-ecosplay-messenger]
command=docker compose -f /var/www/crm-ecosplay/docker-compose-prod.yml exec php php bin/console messenger:consume async --time-limit=3600 --memory-limit=256M --limit=500
directory=/var/www/crm-ecosplay
user=bot
numprocs=2
process_name=%(program_name)s_%(process_num)02d
autostart=true
autorestart=true
startsecs=0
startretries=10
stdout_logfile=/var/log/supervisor/crm-ecosplay-messenger-%(process_num)02d.log
stderr_logfile=/var/log/supervisor/crm-ecosplay-messenger-%(process_num)02d-error.log

21
ansible/pgbouncer.ini.j2 Normal file
View File

@@ -0,0 +1,21 @@
[databases]
crm-ecosplay = host=db-master port=5432 dbname=crm-ecosplay
crm-ecosplay_readonly = host=db-slave port=5432 dbname=crm-ecosplay
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = plain
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 200
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
server_lifetime = 3600
server_idle_timeout = 600
log_connections = 0
log_disconnections = 0
admin_users = crm-ecosplay
stats_users = crm-ecosplay

1
ansible/userlist.txt.j2 Normal file
View File

@@ -0,0 +1 @@
"crm-ecosplay" "{{ db_password }}"

50
ansible/vault.yml Normal file
View File

@@ -0,0 +1,50 @@
cloudflare_api_token: cfk_wXHshV0AmOoKbWE19CIPoWJBeUCtSDUXo2mgkGuNe99eeac8
cloudflare_zone_id: f9e2a44aa512b455c99286d2ae845862
app_secret: 533326ee6417086112c42b1fc1843ff1db82b881988f87442118178ea4469cc2
mailer_dsn: ses+smtp://AKIAWTT2T22CWBRBBDYN:BBdgb6KxRQ8mNcpWFJsZCJxbSGNdgLhKFiITMErfBlQP@default?region=eu-west-3&timeout=120
stripe_pk: pk_live_51SUA1rP4ub49xK2ThoRH8efqGYNi1hrcWMzrqmDtJpMv12cmTzLa8ncJLUKLbOQNZTkm1jgptLfwt4hxEGqkVsHB00AK3ieZNl
stripe_sk: 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
stripe_webhook_secret: whsec_ivqY8C05aJ9mho2N3opJLriO2AuxIqOi
stripe_webhook_secret_connect: whsec_0sG9BPYDysxQst9W6FTp44GpwtO2F6WQ
smime_passphrase: 'EVz5zNV8h4ndSLOCWO9JeaQnIertQm7k'
meilisearch_api_key: b9b0994ee46d9826a4ac58306c171fdd00c03ac6f8ff6a68cdd8f9f977ab415a
meilisearch_master_key: 9e6a86cb9e9b09cdabf30a81e43bdb18892ee0d0ab478a16c50749b8f2ed83e4
db_password: 46eafec68e1e7bc8015790998a2e8ea8b5e31461479588b7
redis_password: 51f7559d1d14a6cf422628537fa562a94481936228e9291d
sonarqube_badge_token: sqb_dc1d0f73af1016295f49d1c56bf60e115e43bf48
keycloak_admin_client_secret: QqYnQc6p9aio4sBJYKNhpPsdrvpUtt2z
docuseal_api: pgAU116mCFmeF7WQSezHqxtZW8V1fgo31u5d2FXoaKe
docuseal_webhooks_secret: CRM_COSLAY
smime_private_key: |
Bag Attributes
localKeyID: 75 15 E3 C2 1D 7B 61 75 99 B9 22 D8 FD A4 19 AC 6B BE 1F 8F
friendlyName: contact@e-cosplay.fr
Key Attributes: <No Attributes>
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC60+PtobUKQsjH
vrRizgwIfR06OkOqYosn17wurYUCZquhGfaRL5nf+qDcOF2liczRfXOsTxnEK0J0
s38JF9zWXTkIms4yfyL9xKOCBEPkNSe03wIcaKQjNB1iuDqW6npUm/ke6VmIAj4q
ig6wwiViqWxkzcE2824RFBI0H4rtkxDNhEXOk6ccVzxz/MwxAvmKHuJQG7LtbysI
KIZcGbNjlm2YmIebVYU44kpI0ZMzfvKsfy9eWX3VyYubpL4Rwsc9I+Z2jsun+nEc
4heHamNu8NrKHWQP802NZvRkMR7t39APYnGcEOBe6eagDkn9DQpZrkxfXYUcteBA
5y3RVBxJAgMBAAECggEASCuXNlJL0blnv0WLgEmRO2w9cKNWRUkI8Wy0rKFqKrnH
nhWd+orVWcEDB98cSdi4Dsrz9sGdtfpyEwvNzJDzxblAUXMfqUEUnswpz41fPpZM
DuJA+gFwOoskuOBq/aZ5eBrCeu7wn4oiQqOPNjfhYkIJdc1H0Lbrs1w6BfmTXN9S
ilSL4GMv7YpEBPt86wiReIbE0DIqaR9Vm1UupkJe7WMc9LrxW6OM4kD5xQTFdJLC
n6QbbX+TE3q+d6TrckK8PeBm+gIX2+NdEuuz7gpZGSybfqN1wqbkziUsifi+7gQi
pxwxhDmCQ08DHplcBdjg1ZldVhaAdHEqCyWZtIZQGwKBgQD9RNXOskPhGBP28oZz
a7APhROKHcEtb9BRj3mwzwIMr3SzJjoBI4SfRR3QcU8jD2uCLNio/cSqjAJVyk2f
Z9wDONnXy4aNrsze0Fx+0t1VqIOVocFDJJHndHCiq2HHorqArvHtZxkj/0xIUx+K
nVltevm7qLSAtR/vWTM1c9Zp/wKBgQC816PZxNEpyg5mgO+3gn5yONsObh2ZyZ/v
KPEmFBjl69AfAxKeaEZaj35RmzdnAFKlv9QzNdAM67+oOIThcaQ+diyNUQegaJwc
amUZHWjQ7RAJNM7lQz85zn9s/MZVC8iNF7xlPWK8Y/ifMwk5e+pAQ79PVwxKiW8y
doXv1uuptwKBgQCPfVJNGr5OZx/YhysZdWd4Q4MAez8ZCQJTUyA0xfN5UGajoQK3
5nGa8sOjdq7JRFE6nUHa7HthzJT+GfHEYElMIgd5tTt0RhaYHiBpUfpIL4LtYYln
Cu8G3Pd7kfGB83YKthMm14Rs2pVTXJTAKcmit0uJwFkTmoqPRlA29nwB7QKBgQCp
iVtzxcwWnW1iPz13XfNRbpPTl66Bg89PMk2VZxOXT6cQQHz1tB39bbf1YclScqrA
XcEPnK9E+l2dk+hQUesnnIaQ9H9JVsFYjXhoHMweTcZIzZ01nLLlnesumJ0ieyq4
6OddaMf85C+wFWyhrAr5WRknDLpfLJWlE11ZnnUCbwKBgQCXOLstj7bEi8N3H8PV
UQlkFKrAmmEE4i3Y3Qqa1DMbm2XJBzPQQJIrDlCeIWs0jk0upOY34W+deWH5gbvW
a/6bpvSzbWqgFD5DnayVkb7CvgEg+CKdUoUumRSs38pet29c/pdKPUxhMq8voIEg
HpnyJcapAiLw0hv4ql380onf0A==
-----END PRIVATE KEY-----

View File

@@ -1,10 +1,149 @@
import './stimulus_bootstrap.js';
/*
* Welcome to your app's main JavaScript file!
*
* This file will be included onto the page via the importmap() Twig function,
* which should already be in your base.html.twig.
*/
import './styles/app.css';
import "./app.scss"
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
// Membre / Super Admin : mutuellement exclusif
document.addEventListener('DOMContentLoaded', () => {
const memberCheckbox = document.querySelector('input[value="gp_member"]');
const adminCheckbox = document.querySelector('input[value="super_admin_asso"]');
if (memberCheckbox && adminCheckbox) {
memberCheckbox.addEventListener('change', () => {
if (memberCheckbox.checked) {
document.querySelectorAll('input[name="groups[]"]').forEach(cb => {
if (cb !== memberCheckbox) {
cb.checked = false;
}
});
}
});
adminCheckbox.addEventListener('change', () => {
if (adminCheckbox.checked) {
memberCheckbox.checked = false;
document.querySelectorAll('input[name="groups[]"]').forEach(cb => {
if (cb !== memberCheckbox) {
cb.checked = true;
}
});
}
});
}
// Stats period selector
const periodSelect = document.getElementById('stats-period-select');
const customRange = document.getElementById('stats-custom-range');
if (periodSelect && customRange) {
periodSelect.addEventListener('change', () => {
customRange.classList.toggle('hidden', periodSelect.value !== 'custom');
});
}
// data-confirm
document.querySelectorAll('form[data-confirm]').forEach(form => {
form.addEventListener('submit', (e) => {
if (!confirm(form.dataset.confirm)) {
e.preventDefault();
}
});
});
// Sidebar dropdown toggle
document.querySelectorAll('.sidebar-dropdown-btn').forEach(btn => {
btn.addEventListener('click', () => {
const menu = btn.nextElementSibling;
const arrow = btn.querySelector('.sidebar-dropdown-arrow');
menu?.classList.toggle('hidden');
arrow?.classList.toggle('rotate-180');
});
});
// Mobile sidebar toggle
const sidebarToggle = document.getElementById('admin-sidebar-toggle');
const sidebar = document.getElementById('admin-sidebar');
const overlay = document.getElementById('admin-overlay');
if (sidebarToggle && sidebar && overlay) {
sidebarToggle.addEventListener('click', () => {
sidebar.classList.toggle('open');
});
overlay.addEventListener('click', () => {
sidebar.classList.remove('open');
});
}
// Mobile menu toggle (public)
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
const mobileMenu = document.getElementById('mobile-menu');
const menuIconOpen = document.getElementById('menu-icon-open');
const menuIconClose = document.getElementById('menu-icon-close');
if (mobileMenuBtn && mobileMenu) {
mobileMenuBtn.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
menuIconOpen?.classList.toggle('hidden');
menuIconClose?.classList.toggle('hidden');
});
}
// Cookie banner
const cookieBanner = document.getElementById('cookie-banner');
const cookieAccept = document.getElementById('cookie-accept');
const cookieRefuse = document.getElementById('cookie-refuse');
if (cookieBanner && !localStorage.getItem('cookie_consent')) {
cookieBanner.classList.remove('hidden');
}
cookieAccept?.addEventListener('click', () => {
localStorage.setItem('cookie_consent', 'accepted');
cookieBanner?.classList.add('hidden');
});
cookieRefuse?.addEventListener('click', () => {
localStorage.setItem('cookie_consent', 'refused');
cookieBanner?.classList.add('hidden');
});
// Search (customers & revendeurs)
const setupSearch = (inputId, resultsId, searchUrl, linkPrefix) => {
const input = document.getElementById(inputId);
const results = document.getElementById(resultsId);
if (!input || !results) return;
let debounce;
input.addEventListener('input', () => {
clearTimeout(debounce);
const q = input.value.trim();
if (q.length < 2) {
results.classList.add('hidden');
results.innerHTML = '';
return;
}
debounce = setTimeout(async () => {
const resp = await fetch(`${searchUrl}?q=${encodeURIComponent(q)}`);
const hits = await resp.json();
if (hits.length === 0) {
results.innerHTML = '<div class="px-4 py-3 text-xs text-gray-400">Aucun resultat.</div>';
} else {
results.innerHTML = hits.map(h =>
`<a href="${linkPrefix}${h.id}" class="block px-4 py-2 hover:bg-gray-50 border-b border-gray-100 text-xs">
<span class="font-bold">${h.fullName || h.raisonSociale || (h.firstName + ' ' + h.lastName)}</span>
${h.email ? `<span class="text-gray-400 ml-2">${h.email}</span>` : ''}
${h.codeRevendeur ? `<span class="ml-2 px-1 py-0.5 bg-gray-900 text-[#fabf04] text-[9px] font-bold">${h.codeRevendeur}</span>` : ''}
</a>`
).join('');
}
results.classList.remove('hidden');
}, 300);
});
document.addEventListener('click', (e) => {
if (!results.contains(e.target) && e.target !== input) {
results.classList.add('hidden');
}
});
};
setupSearch('search-customers', 'search-results', '/admin/clients/search', '/admin/clients/');
setupSearch('search-revendeurs', 'search-results-revendeurs', '/admin/revendeurs/search', '/admin/revendeurs/');
});

28
assets/app.scss Normal file
View File

@@ -0,0 +1,28 @@
@import "tailwindcss";
.page-container {
width: 90%;
max-width: 1200px;
margin: 0 auto;
padding: 2rem 0;
@media (min-width: 768px) {
width: 80%;
padding: 3rem 0;
}
}
.heading-page {
border-bottom: 4px solid #111827;
display: inline-block;
padding-bottom: 0.25rem;
}
.admin-content .page-container {
width: 95%;
max-width: none;
@media (min-width: 768px) {
width: 90%;
}
}

View File

@@ -1,15 +0,0 @@
{
"controllers": {
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
"fetch": "eager"
},
"mercure-turbo-stream": {
"enabled": false,
"fetch": "eager"
}
}
},
"entrypoints": []
}

View File

@@ -1,81 +0,0 @@
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
// Use `form.requestSubmit()` to ensure that the submit event is triggered. Using `form.submit()` will not trigger the event
// and thus this event-listener will not be executed.
document.addEventListener('submit', function (event) {
generateCsrfToken(event.target);
}, true);
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
document.addEventListener('turbo:submit-start', function (event) {
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
Object.keys(h).map(function (k) {
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
});
});
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
document.addEventListener('turbo:submit-end', function (event) {
removeCsrfToken(event.detail.formSubmission.formElement);
});
export function generateCsrfToken (formElement) {
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return;
}
let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
let csrfToken = csrfField.value;
if (!csrfCookie && nameCheck.test(csrfToken)) {
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
}
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
if (csrfCookie && tokenCheck.test(csrfToken)) {
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
}
}
export function generateCsrfHeaders (formElement) {
const headers = {};
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return headers;
}
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
headers[csrfCookie] = csrfField.value;
}
return headers;
}
export function removeCsrfToken (formElement) {
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return;
}
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
}
}
/* stimulusFetch: 'lazy' */
export default 'csrf-protection-controller';

View File

@@ -1,16 +0,0 @@
import { Controller } from '@hotwired/stimulus';
/*
* This is an example Stimulus controller!
*
* Any element with a data-controller="hello" attribute will cause
* this controller to be executed. The name "hello" comes from the filename:
* hello_controller.js -> "hello"
*
* Delete this file or adapt it for your use!
*/
export default class extends Controller {
connect() {
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
}
}

View File

@@ -1,5 +0,0 @@
import { startStimulusApp } from '@symfony/stimulus-bundle';
const app = startStimulusApp();
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);

View File

@@ -1,3 +0,0 @@
body {
background-color: skyblue;
}

1468
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
services:
###> doctrine/doctrine-bundle ###
database:
ports:
- "5432"
###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
mailer:
image: axllent/mailpit
ports:
- "1025"
- "8025"
environment:
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
###< symfony/mailer ###

View File

@@ -1,25 +0,0 @@
services:
###> doctrine/doctrine-bundle ###
database:
image: postgres:${POSTGRES_VERSION:-16}-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-app}
# You should definitely change the password in production
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
POSTGRES_USER: ${POSTGRES_USER:-app}
healthcheck:
test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"]
timeout: 5s
retries: 5
start_period: 60s
volumes:
- database_data:/var/lib/postgresql/data:rw
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
# - ./docker/db/data:/var/lib/postgresql/data:rw
###< doctrine/doctrine-bundle ###
volumes:
###> doctrine/doctrine-bundle ###
database_data:
###< doctrine/doctrine-bundle ###

View File

@@ -7,48 +7,69 @@
"php": ">=8.4",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/doctrine-bundle": "^3.2",
"doctrine/doctrine-bundle": "^3.2.2",
"doctrine/doctrine-migrations-bundle": "^4.0",
"doctrine/orm": "^3.6",
"phpdocumentor/reflection-docblock": "^6.0",
"phpstan/phpdoc-parser": "^2.3",
"doctrine/orm": "^3.6.2",
"docusealco/docuseal-php": "^1.0",
"dompdf/dompdf": ">=3.1.5",
"endroid/qr-code-bundle": ">=6.1",
"knplabs/knp-paginator-bundle": "^6.10",
"knpuniversity/oauth2-client-bundle": "^2.20.2",
"league/flysystem-bundle": "^3.6.2",
"liip/imagine-bundle": "^2.17.1",
"meilisearch/meilisearch-php": "^1.16",
"mobiledetect/mobiledetectlib": ">=4.8.10",
"nelmio/security-bundle": "^3.9",
"phpdocumentor/reflection-docblock": "^6.0.3",
"phpstan/phpdoc-parser": "^2.3.2",
"scheb/2fa-backup-code": "^8.5",
"scheb/2fa-bundle": "^8.5",
"scheb/2fa-email": "^8.5",
"scheb/2fa-google-authenticator": "^8.5",
"spomky-labs/pwa-bundle": "1.5.7",
"stevenmaguire/oauth2-keycloak": "^6.1.1",
"stripe/stripe-php": ">=20",
"symfony/amazon-mailer": "8.0.*",
"symfony/asset": "8.0.*",
"symfony/asset-mapper": "8.0.*",
"symfony/console": "8.0.*",
"symfony/doctrine-messenger": "8.0.*",
"symfony/dotenv": "8.0.*",
"symfony/expression-language": "8.0.*",
"symfony/flex": "^2",
"symfony/flex": "^2.10",
"symfony/form": "8.0.*",
"symfony/framework-bundle": "8.0.*",
"symfony/http-client": "8.0.*",
"symfony/intl": "8.0.*",
"symfony/mailer": "8.0.*",
"symfony/mime": "8.0.*",
"symfony/monolog-bundle": "^3.0|^4.0",
"symfony/monolog-bundle": "^3.0|^4.0.1",
"symfony/notifier": "8.0.*",
"symfony/process": "8.0.*",
"symfony/property-access": "8.0.*",
"symfony/property-info": "8.0.*",
"symfony/rate-limiter": "8.0.*",
"symfony/redis-messenger": "8.0.*",
"symfony/runtime": "8.0.*",
"symfony/security-bundle": "8.0.*",
"symfony/serializer": "8.0.*",
"symfony/stimulus-bundle": "^2.34",
"symfony/string": "8.0.*",
"symfony/translation": "8.0.*",
"symfony/twig-bundle": "8.0.*",
"symfony/ux-turbo": "^2.34",
"symfony/validator": "8.0.*",
"symfony/web-link": "8.0.*",
"symfony/yaml": "8.0.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
"twig/cssinliner-extra": "^3.24",
"twig/extra-bundle": "^2.12|^3.24",
"twig/inky-extra": "^3.24",
"twig/twig": "^2.12|^3.24",
"vich/uploader-bundle": "^2.9.2"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
"symfony/runtime": true,
"endroid/installer": true
},
"bump-after-update": true,
"sort-packages": true
@@ -98,11 +119,13 @@
}
},
"require-dev": {
"phpunit/phpunit": "^13.0",
"friendsofphp/php-cs-fixer": "^3.94.2",
"phpstan/phpstan": "^2.1.45",
"phpunit/phpunit": "^13.0.5",
"symfony/browser-kit": "8.0.*",
"symfony/css-selector": "8.0.*",
"symfony/debug-bundle": "8.0.*",
"symfony/maker-bundle": "^1.0",
"symfony/maker-bundle": "^1.67",
"symfony/stopwatch": "8.0.*",
"symfony/web-profiler-bundle": "8.0.*"
}

9615
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,10 +7,17 @@ return [
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Endroid\QrCodeBundle\EndroidQrCodeBundle::class => ['all' => true],
Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true],
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
League\FlysystemBundle\FlysystemBundle::class => ['all' => true],
Liip\ImagineBundle\LiipImagineBundle::class => ['all' => true],
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
SpomkyLabs\PwaBundle\SpomkyLabsPwaBundle::class => ['all' => true],
Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true],
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
];

View File

@@ -0,0 +1,125 @@
Bag Attributes
localKeyID: 75 15 E3 C2 1D 7B 61 75 99 B9 22 D8 FD A4 19 AC 6B BE 1F 8F
friendlyName: contact@e-cosplay.fr
subject=CN = contact@e-cosplay.fr
issuer=C = IT, ST = Bergamo, L = Ponte San Pietro, O = Actalis S.p.A., CN = Actalis Client Authentication CA G3
-----BEGIN CERTIFICATE-----
MIIF7TCCA9WgAwIBAgIQVGwKj9U0zzzn3LBvYN7iODANBgkqhkiG9w0BAQsFADCB
gTELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB0JlcmdhbW8xGTAXBgNVBAcMEFBvbnRl
IFNhbiBQaWV0cm8xFzAVBgNVBAoMDkFjdGFsaXMgUy5wLkEuMSwwKgYDVQQDDCNB
Y3RhbGlzIENsaWVudCBBdXRoZW50aWNhdGlvbiBDQSBHMzAeFw0yNjAyMDEwOTM4
NTdaFw0yNzAyMDEwOTM4NTdaMB8xHTAbBgNVBAMMFGNvbnRhY3RAZS1jb3NwbGF5
LmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAutPj7aG1CkLIx760
Ys4MCH0dOjpDqmKLJ9e8Lq2FAmaroRn2kS+Z3/qg3DhdpYnM0X1zrE8ZxCtCdLN/
CRfc1l05CJrOMn8i/cSjggRD5DUntN8CHGikIzQdYrg6lup6VJv5HulZiAI+KooO
sMIlYqlsZM3BNvNuERQSNB+K7ZMQzYRFzpOnHFc8c/zMMQL5ih7iUBuy7W8rCCiG
XBmzY5ZtmJiHm1WFOOJKSNGTM37yrH8vXll91cmLm6S+EcLHPSPmdo7Lp/pxHOIX
h2pjbvDayh1kD/NNjWb0ZDEe7d/QD2JxnBDgXunmoA5J/Q0KWa5MX12FHLXgQOct
0VQcSQIDAQABo4IBwDCCAbwwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBS+l6mq
hL+AvxBTfQky+eEuMhvPdzB+BggrBgEFBQcBAQRyMHAwOwYIKwYBBQUHMAKGL2h0
dHA6Ly9jYWNlcnQuYWN0YWxpcy5pdC9jZXJ0cy9hY3RhbGlzLWF1dGNsaWczMDEG
CCsGAQUFBzABhiVodHRwOi8vb2NzcDA5LmFjdGFsaXMuaXQvVkEvQVVUSENMLUcz
MB8GA1UdEQQYMBaBFGNvbnRhY3RAZS1jb3NwbGF5LmZyMFIGA1UdIARLMEkwPAYG
K4EfEAEBMDIwMAYIKwYBBQUHAgEWJGh0dHBzOi8vd3d3LmFjdGFsaXMuaXQvYXJl
YS1kb3dubG9hZDAJBgdngQwBBQECMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF
BQcDBDBIBgNVHR8EQTA/MD2gO6A5hjdodHRwOi8vY3JsMDkuYWN0YWxpcy5pdC9S
ZXBvc2l0b3J5L0FVVEhDTC1HMy9nZXRMYXN0Q1JMMB0GA1UdDgQWBBR1FePCHXth
dZm5Itj9pBmsa74fjzAOBgNVHQ8BAf8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIB
AHU6pUBlyDdhCM4pya79GlvPDn64JL9WzWHZnKbMBFQBwmXCuvKZYK5c3aMhw7QV
+XIMPvEdzkhFkhBf4/t6ZGE3LZjn1G+pDWA+6rWe/yhhZKgtbR6ll02K0RUi9d5A
GukCOpSEaOMDE7Mn0inG+Rozx28PR/0qRVTCoBN7aCH7Z08BSigwIXsoXo6aglrv
9OI6IYRC4/x2oOA25g6ONl8I10mba17QLKY0OLaPYA23+wK6cd5XaQ+Xbl7anOAC
gWv3ZkGzLq/54jqhDfmu42HnwDD8PC+uXR/4JGFtQncAu9PYua8BW7STnAv2dY9E
j3p8BFzUH6LDijMRZxexZR97X+WHkA2Pei7eMEKmF5EpTRm7/q9LZLBiwa2aqfdf
5Cwjeq/pM96JHTj2wlA84e2EF+5yqslRKQ//lLtXLnch6oVi4hrhvCnFqkMnJt66
Doxwm0U0X3o4TdHgPGhFwnxXWEstlOvhAK3fCkY2/ZKk9PUFnc4nsOr1c7ckgLKO
ks76UBmXPQnrsj66ahwiXaQDMPk14/ponWjPdpy6cvE0zIcA8G+zGJLXh/fRro9P
Bo+2YpoM9fA7W4vt3NwJyG+GdALEa0TNBNimppe1usdIFJ3yrdAaa8gJJ49qmGvE
0mxpxuXxY7nMiI5G+H4h1rx5r2VlyRJrpxBmC+zgtvMD
-----END CERTIFICATE-----
Bag Attributes
2.16.840.1.113894.746875.1.1: <Unsupported tag 6>
<Unsupported tag 6>
friendlyName: Actalis Client Authentication CA G3
subject=C = IT, ST = Bergamo, L = Ponte San Pietro, O = Actalis S.p.A., CN = Actalis Client Authentication CA G3
issuer=C = IT, L = Milan, O = Actalis S.p.A./03358520967, CN = Actalis Authentication Root CA
-----BEGIN CERTIFICATE-----
MIIHbTCCBVWgAwIBAgIQFxA+3j2KHLXKBlGT58pDazANBgkqhkiG9w0BAQsFADBr
MQswCQYDVQQGEwJJVDEOMAwGA1UEBwwFTWlsYW4xIzAhBgNVBAoMGkFjdGFsaXMg
Uy5wLkEuLzAzMzU4NTIwOTY3MScwJQYDVQQDDB5BY3RhbGlzIEF1dGhlbnRpY2F0
aW9uIFJvb3QgQ0EwHhcNMjAwNzA2MDg0NTQ3WhcNMzAwOTIyMTEyMjAyWjCBgTEL
MAkGA1UEBhMCSVQxEDAOBgNVBAgMB0JlcmdhbW8xGTAXBgNVBAcMEFBvbnRlIFNh
biBQaWV0cm8xFzAVBgNVBAoMDkFjdGFsaXMgUy5wLkEuMSwwKgYDVQQDDCNBY3Rh
bGlzIENsaWVudCBBdXRoZW50aWNhdGlvbiBDQSBHMzCCAiIwDQYJKoZIhvcNAQEB
BQADggIPADCCAgoCggIBAO3mh5ahwaS27cJCVfc/Dw8iYF8T4KZDiIZJkXkcGy8a
UA/cRgHu9ro6hsxRYe/ED4AIcSlarRh82HqtFSVQs4ZwikQW1V/icCIS91C2IVAG
a1YlKfedqgweqky+bBniUvRevVT0keZOqRTcO5hw007dL6FhYNmlZBt5IaJs1V6I
niRjokOHR++qWgrUGy5LefY6ACs9gZ8Bi0OMK9PZ37pibeQCsdmMRytl4Ej7JVWe
M/BtNIIprHwO1LY0/8InpGOmdG+5LC6xHLzg53B0HvVUqzUQNePUhNwJZFmmTP46
FXovxmH4/SuY5IkXop0eJqjN+dxRHHizngYUk1EaTHUOcLFy4vQ0kxgbjb+GsNg6
M2/6gZZIRk78JPdpotIwHnBNtkp9wPVH61NqdcP7kbPkyLXkNMTtAfydpmNnGqqH
LEvUrK4iBpUPG9C09KOjm9OyhrT2uf5SLzJsee9g79r/rw4hAgcsZtR3YI6fCbRO
JncmD+hgbHCck+9TWcNc1x5xZMgm8UXmoPamkkfceAlVV49QQ5jUTgqneTQHyF1F
2ExXmf47pEIoJMVxloRIXywQuB2uqcIs8/X6tfsMDynFmhfT/0mTrgQ6xt9DIsgm
WuuhvZhLReWS7oeKxnyqscuGeTMXnLs7fjGZq0inyhnlznhA/4rl+WdNjNaO4jEv
AgMBAAGjggH0MIIB8DAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFFLYiDrI
n3hm7YnzezhwlMkCAjbQMEEGCCsGAQUFBwEBBDUwMzAxBggrBgEFBQcwAYYlaHR0
cDovL29jc3AwNS5hY3RhbGlzLml0L1ZBL0FVVEgtUk9PVDBFBgNVHSAEPjA8MDoG
BFUdIAAwMjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cuYWN0YWxpcy5pdC9hcmVh
LWRvd25sb2FkMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDCB4wYDVR0f
BIHbMIHYMIGWoIGToIGQhoGNbGRhcDovL2xkYXAwNS5hY3RhbGlzLml0L2NuJTNk
QWN0YWxpcyUyMEF1dGhlbnRpY2F0aW9uJTIwUm9vdCUyMENBLG8lM2RBY3RhbGlz
JTIwUy5wLkEuJTJmMDMzNTg1MjA5NjcsYyUzZElUP2NlcnRpZmljYXRlUmV2b2Nh
dGlvbkxpc3Q7YmluYXJ5MD2gO6A5hjdodHRwOi8vY3JsMDUuYWN0YWxpcy5pdC9S
ZXBvc2l0b3J5L0FVVEgtUk9PVC9nZXRMYXN0Q1JMMB0GA1UdDgQWBBS+l6mqhL+A
vxBTfQky+eEuMhvPdzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIB
ACab5xtZDXSzEgPp51X3hICFzULDO2EcV8em5hLfSCKxZR9amCnjcODVfMbaKfdU
ZXtevMIIZmHgkz9dBan7ijGbJXjZCPP29zwZGSyCjpfadg5s9hnNCN1r3DGwIHfy
LgbcfffDyV/2wW+XTGbhldnazZsX892q+srRmC8XnX4ygg+eWL/AkHDenvbFuTlJ
vUyd5I7e1nb3dYXMObPu24ZTQ9/K1hSQbs7pqecaptTUjoIDpBUpSp4Us+h1I4MA
WonemKYoPS9f0y65JrRCKcfsKSI+1kwPSanDDMiydKzeo46XrS0hlA5NzQjqUJ7U
suGvPtDvknqc0v03nNXBnUjejYtvwO3sEDXdUW5m9kjNqlQZXzdHumZJVqPUGKTW
cn9Hf3d7qbCmmxPXjQoNUuHg56fLCanZWkEO4SP1GAgIA7SyJu/yffv0ts7sBFrS
TD3L2mCAXM3Y8BfblvvDSf2bvySm/fPe9brmuzrCXsTxUQc1+/z5ydvzV3E3cLnU
oSXP6XfXNyEVO6sPkcUSnISHM798xLkCTB5EkjPCjPE2zs4v9L9JVOkkskvW6RnW
WccdfR3fELNHL/kep8re6IbbYs8Hn5GM0Ohs8CMDPYEox+QX/6/SnOfyaqqSilBo
nMQBstsymBBgdEKO+tTHHCMnJQVvZn7jRQ20wXgxMrvN
-----END CERTIFICATE-----
Bag Attributes
2.16.840.1.113894.746875.1.1: <Unsupported tag 6>
friendlyName: Actalis Authentication Root CA
subject=C = IT, L = Milan, O = Actalis S.p.A./03358520967, CN = Actalis Authentication Root CA
issuer=C = IT, L = Milan, O = Actalis S.p.A./03358520967, CN = Actalis Authentication Root CA
-----BEGIN CERTIFICATE-----
MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE
BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w
MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290
IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC
SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1
ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv
UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX
4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9
KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/
gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb
rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ
51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F
be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe
KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F
v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn
fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7
jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz
ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt
ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL
e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70
jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz
WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V
SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j
pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX
X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok
fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R
K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU
ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU
LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT
LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==
-----END CERTIFICATE-----

Binary file not shown.

View File

@@ -0,0 +1,32 @@
Bag Attributes
localKeyID: 75 15 E3 C2 1D 7B 61 75 99 B9 22 D8 FD A4 19 AC 6B BE 1F 8F
friendlyName: contact@e-cosplay.fr
Key Attributes: <No Attributes>
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC60+PtobUKQsjH
vrRizgwIfR06OkOqYosn17wurYUCZquhGfaRL5nf+qDcOF2liczRfXOsTxnEK0J0
s38JF9zWXTkIms4yfyL9xKOCBEPkNSe03wIcaKQjNB1iuDqW6npUm/ke6VmIAj4q
ig6wwiViqWxkzcE2824RFBI0H4rtkxDNhEXOk6ccVzxz/MwxAvmKHuJQG7LtbysI
KIZcGbNjlm2YmIebVYU44kpI0ZMzfvKsfy9eWX3VyYubpL4Rwsc9I+Z2jsun+nEc
4heHamNu8NrKHWQP802NZvRkMR7t39APYnGcEOBe6eagDkn9DQpZrkxfXYUcteBA
5y3RVBxJAgMBAAECggEASCuXNlJL0blnv0WLgEmRO2w9cKNWRUkI8Wy0rKFqKrnH
nhWd+orVWcEDB98cSdi4Dsrz9sGdtfpyEwvNzJDzxblAUXMfqUEUnswpz41fPpZM
DuJA+gFwOoskuOBq/aZ5eBrCeu7wn4oiQqOPNjfhYkIJdc1H0Lbrs1w6BfmTXN9S
ilSL4GMv7YpEBPt86wiReIbE0DIqaR9Vm1UupkJe7WMc9LrxW6OM4kD5xQTFdJLC
n6QbbX+TE3q+d6TrckK8PeBm+gIX2+NdEuuz7gpZGSybfqN1wqbkziUsifi+7gQi
pxwxhDmCQ08DHplcBdjg1ZldVhaAdHEqCyWZtIZQGwKBgQD9RNXOskPhGBP28oZz
a7APhROKHcEtb9BRj3mwzwIMr3SzJjoBI4SfRR3QcU8jD2uCLNio/cSqjAJVyk2f
Z9wDONnXy4aNrsze0Fx+0t1VqIOVocFDJJHndHCiq2HHorqArvHtZxkj/0xIUx+K
nVltevm7qLSAtR/vWTM1c9Zp/wKBgQC816PZxNEpyg5mgO+3gn5yONsObh2ZyZ/v
KPEmFBjl69AfAxKeaEZaj35RmzdnAFKlv9QzNdAM67+oOIThcaQ+diyNUQegaJwc
amUZHWjQ7RAJNM7lQz85zn9s/MZVC8iNF7xlPWK8Y/ifMwk5e+pAQ79PVwxKiW8y
doXv1uuptwKBgQCPfVJNGr5OZx/YhysZdWd4Q4MAez8ZCQJTUyA0xfN5UGajoQK3
5nGa8sOjdq7JRFE6nUHa7HthzJT+GfHEYElMIgd5tTt0RhaYHiBpUfpIL4LtYYln
Cu8G3Pd7kfGB83YKthMm14Rs2pVTXJTAKcmit0uJwFkTmoqPRlA29nwB7QKBgQCp
iVtzxcwWnW1iPz13XfNRbpPTl66Bg89PMk2VZxOXT6cQQHz1tB39bbf1YclScqrA
XcEPnK9E+l2dk+hQUesnnIaQ9H9JVsFYjXhoHMweTcZIzZ01nLLlnesumJ0ieyq4
6OddaMf85C+wFWyhrAr5WRknDLpfLJWlE11ZnnUCbwKBgQCXOLstj7bEi8N3H8PV
UQlkFKrAmmEE4i3Y3Qqa1DMbm2XJBzPQQJIrDlCeIWs0jk0upOY34W+deWH5gbvW
a/6bpvSzbWqgFD5DnayVkb7CvgEg+CKdUoUumRSs38pet29c/pdKPUxhMq8voIEg
HpnyJcapAiLw0hv4ql380onf0A==
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,10 @@
nelmio_security:
csp:
enforce:
script-src:
- 'wasm-unsafe-eval'
- 'http://localhost:5173/'
style-src:
- 'unsafe-inline'
connect-src:
- 'ws://localhost:5173'

View File

@@ -0,0 +1,7 @@
# Read the documentation at https://github.com/thephpleague/flysystem-bundle/blob/master/docs/1-getting-started.md
flysystem:
storages:
default.storage:
adapter: 'local'
options:
directory: '%kernel.project_dir%/var/storage/default'

View File

@@ -1,6 +1,8 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
trusted_proxies: 'REMOTE_ADDR'
trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port']
# Note that the session will be started ONLY if you read or write from it.
session: true

View File

@@ -0,0 +1,10 @@
services:
Psr\Http\Message\RequestFactoryInterface: '@http_discovery.psr17_factory'
Psr\Http\Message\ResponseFactoryInterface: '@http_discovery.psr17_factory'
Psr\Http\Message\ServerRequestFactoryInterface: '@http_discovery.psr17_factory'
Psr\Http\Message\StreamFactoryInterface: '@http_discovery.psr17_factory'
Psr\Http\Message\UploadedFileFactoryInterface: '@http_discovery.psr17_factory'
Psr\Http\Message\UriFactoryInterface: '@http_discovery.psr17_factory'
http_discovery.psr17_factory:
class: Http\Discovery\Psr17Factory

View File

@@ -0,0 +1,9 @@
knpu_oauth2_client:
clients:
keycloak:
type: keycloak
auth_server_url: '%env(OAUTH_KEYCLOAK_URL)%'
realm: '%env(OAUTH_KEYCLOAK_REALM)%'
client_id: '%env(OAUTH_KEYCLOAK_CLIENT_ID)%'
client_secret: '%env(OAUTH_KEYCLOAK_CLIENT_SECRET)%'
redirect_route: connect_keycloak_check

View File

@@ -0,0 +1,49 @@
liip_imagine:
driver: "imagick"
twig:
mode: lazy
webp:
generate: true
quality: 85
filter_sets:
webp:
quality: 85
format: 'webp'
filters:
strip: ~
avatar_partner:
quality: 75
format: 'webp'
filters:
strip: ~
thumbnail: { size: [ 128, 128 ], mode: inset }
members:
quality: 75
format: 'webp'
filters:
strip: ~
thumbnail: { size: [ 128, 128 ], mode: inset }
navbar_logo:
quality: 85
format: 'webp'
filters:
strip: ~
thumbnail: { size: [ 48, 48 ], mode: inset }
avatar:
quality: 80
format: 'webp'
filters:
strip: ~
downscale: { max: [ 128, 128 ] }
avatar_small:
quality: 80
format: 'webp'
filters:
strip: ~
downscale: { max: [ 40, 40 ] }
logo:
quality: 85
format: 'webp'
filters:
strip: ~
thumbnail: { size: [ 99, 56 ], mode: inset }

View File

@@ -0,0 +1,97 @@
nelmio_security:
clickjacking:
paths:
'^/.*': DENY
content_type:
nosniff: true
referrer_policy:
enabled: true
policies:
- 'no-referrer'
- 'strict-origin-when-cross-origin'
csp:
enforce:
level1_fallback: false
browser_adaptive:
enabled: false
report-uri: '%router.request_context.base_url%/my-csp-report'
frame-ancestors:
- 'self'
frame-src:
- 'self'
- 'https://stripe.com'
- 'https://*.stripe.com'
- 'https://js.stripe.com'
- 'https://cloudflare.com'
- 'https://*.cloudflareinsights.com'
- 'https://challenges.cloudflare.com'
script-src:
- 'self'
- 'https://static.cloudflareinsights.com'
- 'https://challenges.cloudflare.com'
- 'https://cdn.jsdelivr.net'
- 'https://js.stripe.com'
- 'unsafe-inline'
style-src:
- 'self'
- 'https://fonts.googleapis.com'
- 'https://cdnjs.cloudflare.com'
- 'https://cdn.jsdelivr.net'
- 'unsafe-inline'
img-src:
- 'self'
- 'data:'
- 'https://*.tile.openstreetmap.org'
- 'https://*.basemaps.cartocdn.com'
- 'https://cdn.jsdelivr.net'
worker-src:
- 'self'
- 'blob:'
connect-src:
- 'self'
- 'https://cloudflareinsights.com'
- 'https://static.cloudflareinsights.com'
- 'https://challenges.cloudflare.com'
- 'https://nominatim.openstreetmap.org'
- 'https://cdn.jsdelivr.net'
- 'https://api.stripe.com'
font-src:
- 'self'
- 'https://cdnjs.cloudflare.com'
- 'https://fonts.googleapis.com'
- 'https://fonts.gstatic.com'
object-src:
- 'none'
form-action:
- 'self'
- 'https://auth.esy-web.dev'
- 'https://*.stripe.com'
- 'https://checkout.stripe.com'
block-all-mixed-content: true
permissions_policy:
enabled: true
policies:
payment: ['self']
camera: ['self']
microphone: []
geolocation: ['self']
external_redirects:
override: /external-redirect
forward_as: redirUrl
log: true
allow_list:
- cloudflareinsights.com
- static.cloudflareinsights.com
- stripe.com
- connect.stripe.com
- checkout.stripe.com
- hooks.stripe.com
- dashboard.stripe.com
- auth.esy-web.dev
- challenges.cloudflare.com

View File

@@ -0,0 +1,11 @@
framework:
asset_mapper:
# The paths to make available to the asset mapper.
paths:
- assets/
missing_import_mode: strict
when@prod:
framework:
asset_mapper:
missing_import_mode: warn

View File

@@ -0,0 +1,19 @@
framework:
cache:
# Nom unique de votre application : utilisé pour calculer des espaces de noms stables pour les clés de cache.
# Ceci est CRUCIAL pour éviter les collisions de clés si plusieurs applications partagent le même serveur de cache (ex: Redis).
# Décommentez et remplacez par une valeur unique à votre projet (ex: "mon_entreprise/mon_app")
prefix_seed: 'e-cosplay/contest' # <-- REMPLACEZ CECI PAR UN NOM UNIQUE À VOTRE PROJET
# En production, utilisez un adaptateur de cache rapide et performant comme Redis.
# Assurez-vous que votre serveur Redis est accessible.
app: cache.adapter.redis
default_redis_provider: '%env(REDIS_DSN)%'
# Vous pouvez également optimiser les pools personnalisés pour la production si besoin.
pools:
my.user_data_cache:
adapter: cache.adapter.redis
my.api_data_cache:
adapter: cache.adapter.redis
vite_cache_pool:
adapter: cache.adapter.redis

View File

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

View File

@@ -0,0 +1,5 @@
when@dev:
debug:
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
# See the "server:dump" command to start a new server.
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"

View File

@@ -0,0 +1,8 @@
nelmio_security:
csp:
enforce:
script-src:
- "http://localhost:5173"
connect-src:
- "ws://localhost:5173/" # Autorise le WebSocket de Vite

View File

@@ -0,0 +1,3 @@
pwa:
serviceworker:
enabled: false

View File

@@ -0,0 +1,54 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '16'
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true
report_fields_where_declared: true
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
identity_generation_preferences:
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
auto_mapping: true
mappings:
App:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
controller_resolver:
auto_mapping: false
when@test:
doctrine:
dbal:
# "TEST_TOKEN" is typically set by ParaTest
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

View File

@@ -0,0 +1,6 @@
doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false

View File

@@ -0,0 +1,7 @@
# Read the documentation at https://github.com/thephpleague/flysystem-bundle/blob/master/docs/1-getting-started.md
flysystem:
storages:
default.storage:
adapter: 'local'
options:
directory: '%kernel.project_dir%/var/storage/default'

View File

@@ -0,0 +1,24 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
http_cache:
enabled: true
default_ttl: 3600
stale_while_revalidate: 3600
stale_if_error: 3600
session:
name: ecosplay_session
cookie_lifetime: 3600
cookie_secure: true
#esi: true
#fragments: true
trusted_proxies: '103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,104.16.0.0/13,104.24.0.0/14,108.162.192.0/18,131.0.72.0/22,141.101.64.0/18,162.158.0.0/15,172.64.0.0/13,173.245.48.0/20,188.114.96.0/20,190.93.240.0/20,197.234.240.0/22,198.41.128.0/17'
trusted_headers: [ 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix' ]
when@test:
framework:
test: true
session:
storage_factory_id: session.storage.factory.mock_file

View File

@@ -0,0 +1,11 @@
services:
Google\Client:
class: Google\Client
calls:
# Authentication with "API key"
- [setDeveloperKey, ['%env(GOOGLE_API_KEY)%']]
# Authentication with "OAuth 2.0" using Client ID & Secret
- [setClientId, ['%env(GOOGLE_CLIENT_ID)%']]
- [setClientSecret, ['%env(GOOGLE_CLIENT_SECRET)%']]
# Authentication with "OAuth 2.0" or "Service account" using JSON
- [setAuthConfig, ['%env(resolve:GOOGLE_AUTH_CONFIG)%']]

View File

@@ -0,0 +1,9 @@
knpu_oauth2_client:
clients:
keycloak:
type: keycloak
auth_server_url: '%env(OAUTH_KEYCLOAK_URL)%'
realm: '%env(OAUTH_KEYCLOAK_REALM)%'
client_id: '%env(OAUTH_KEYCLOAK_CLIENT_ID)%'
client_secret: '%env(OAUTH_KEYCLOAK_CLIENT_SECRET)%'
redirect_route: connect_keycloak_check

View File

@@ -0,0 +1,28 @@
liip_imagine:
driver: "gd"
twig:
mode: lazy
filter_sets:
webp:
quality: 85
format: 'webp'
filters:
strip: ~
avatar_partner:
quality: 75
format: 'webp'
filters:
strip: ~
thumbnail: { size: [ 128, 128 ], mode: inset }
members:
quality: 75
format: 'webp'
filters:
strip: ~
thumbnail: { size: [ 128, 128 ], mode: inset }
logo:
quality: 85
format: 'webp'
filters:
strip: ~
thumbnail: { size: [ 99, 56 ], mode: inset }

View File

@@ -0,0 +1,3 @@
framework:
mailer:
dsn: '%env(MAILER_DSN)%'

View File

@@ -0,0 +1,8 @@
# config/packages/messenger.yaml
framework:
messenger:
transports:
async: "%env(MESSENGER_TRANSPORT_DSN)%"
routing:

View File

@@ -0,0 +1,51 @@
monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
when@dev:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
when@test:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
when@prod:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]

View File

@@ -0,0 +1,10 @@
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization']
expose_headers: ['Link']
max_age: 3600
paths:
'^/': null

View File

@@ -0,0 +1,62 @@
nelmio_security:
# Content Security Policy (CSP)
referrer_policy:
enabled: true
policies:
- 'strict-origin-when-cross-origin'
permissions_policy:
enabled: true
policies:
camera: [self] # Correct : sans les guillemets simples internes
microphone: [self] # Correct
geolocation: [self] # Correct
fullscreen: [self] # Correct
payment: [self] # Correct
# Si tu veux bloquer une fonction pour tout le monde :
usb: []
csp:
hash:
algorithm: 'sha256'
enforce:
default-src: ["'self'"]
worker-src: ["'self'"]
script-src:
- "'self'"
- "nonce"
- "https://sentry.esy-web.dev"
- "https://chat.esy-web.dev"
- "https://static.cloudflareinsights.com"
- "https://challenges.cloudflare.com"
connect-src:
- "'self'"
- "https://sentry.esy-web.dev"
- "https://chat.esy-web.dev"
- "https://auth.esy-web.dev"
- "https://cloudflareinsights.com"
- "https://challenges.cloudflare.com"
- "https://tools-security.esy-web.dev"
- "https://checkout.stripe.com/"
- "https://cdnjs.cloudflare.com"
frame-src:
- "'self'"
- "https://chat.esy-web.dev"
- "https://challenges.cloudflare.com"
- "https://climate.stripe.com/"
style-src:
- "'self'"
- "'unsafe-inline'"
- "https://fonts.googleapis.com"
- "https://chat.esy-web.dev"
- "https://cdnjs.cloudflare.com"
img-src:
- "'self'"
- "data:"
- "https://chat.esy-web.dev"
font-src:
- "'self'"
- "data:"
- 'https://fonts.gstatic.com'
- "https://cdnjs.cloudflare.com/"
frame-ancestors: ["'none'"]
# Optionnel : forcer le passage en HTTPS
upgrade-insecure-requests: false

View File

@@ -0,0 +1,12 @@
framework:
notifier:
chatter_transports:
texter_transports:
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }

View File

@@ -0,0 +1,3 @@
pixel_open_cloudflare_turnstile:
key: "%env(TURNSTILE_KEY)%"
secret: "%env(TURNSTILE_SECRET)%"

View File

@@ -0,0 +1,3 @@
presta_sitemap:
sitemap_file_prefix: 'sitemap'
timetolive: 3600

View File

@@ -0,0 +1,3 @@
framework:
property_info:
with_constructor_extractor: true

View File

@@ -0,0 +1,23 @@
pwa:
asset_compiler: false # Default to true. Will change to false in 2.0.0.
image_processor: 'pwa.image_processor.gd' # Or 'pwa.image_processor.gd'
favicons:
enabled: true
src: '%kernel.project_dir%/public/assets/notif.png'
serviceworker:
enabled: true
scope: "/"
use_cache: false
skip_waiting: true
manifest:
enabled: true
name: "E-Cosplay"
short_name: "PWA"
start_url: "app_home"
display: "standalone"
background_color: "#ffffff"
theme_color: "#4285f4"
categories: ['games','multimedia','social networking']
icons:
- src: '%kernel.project_dir%/public/assets/notif.png'
sizes: [192]

View File

@@ -0,0 +1,10 @@
framework:
router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
#default_uri: http://localhost
when@prod:
framework:
router:
strict_requirements: null

View File

@@ -0,0 +1,50 @@
# config/packages/security.yaml
security:
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# Appelle votre provider d'utilisateurs.
# Ici, nous configurons un provider d'entité pour notre classe Account,
# en spécifiant 'username' comme propriété d'identification.
app_account_provider:
entity:
class: App\Entity\Account
property: email # Utilise le champ 'username' de votre entité Account pour l'authentification
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_account_provider # Utilise le provider que nous avons défini ci-dessus
user_checker: App\Security\UserChecker
form_login:
login_path: app_login # La route vers votre formulaire de connexion (GET)
check_path: app_login # L'URL où le formulaire POST sera soumis
enable_csrf: true # Active la protection CSRF
csrf_token_id: authenticate # ID du jeton CSRF (doit correspondre à celui dans votre Twig)
entry_point: App\Security\AuthenticationEntryPoint
custom_authenticator:
- App\Security\LoginFormAuthenticator
- App\Security\KeycloakAuthenticator
- App\Security\DiscordAuthenticator
logout:
target: app_logout
# Configuration des algorithmes de hachage des mots de passe.
# Symfony choisira automatiquement le meilleur algorithme par défaut si non spécifié,
# mais vous pouvez le configurer explicitement.
password_hashers:
App\Entity\Account: 'auto' # 'auto' sélectionne le meilleur algorithme disponible (recommandé)
# Ou pour spécifier bcrypt explicitement :
# App\Entity\Account:
# algorithm: bcrypt
role_hierarchy:
ROLE_ROOT: [ROLE_ADMIN] # ROLE_ROOT inclut ROLE_ADMIN, qui à son tour inclut ROLE_ARTEMIS
access_control:
- { path: ^/admin, roles: [ROLE_ADMIN] }
- { path: ^/, roles: PUBLIC_ACCESS } # Toutes les autres pages nécessitent une authentification complète

View File

@@ -0,0 +1,36 @@
when@prod:
sentry:
dsn: '%env(SENTRY_DSN)%'
options:
# Add request headers, cookies, IP address and the authenticated user
# see https://docs.sentry.io/platforms/php/data-management/data-collected/ for more info
# send_default_pii: true
ignore_exceptions:
- 'Symfony\Component\ErrorHandler\Error\FatalError'
- 'Symfony\Component\Debug\Exception\FatalErrorException'
#
# # If you are using Monolog, you also need this additional configuration to log the errors correctly:
# # https://docs.sentry.io/platforms/php/guides/symfony/#monolog-integration
# register_error_listener: false
# register_error_handler: false
#
# monolog:
# handlers:
# # Use this only if you don't want to use structured logging and instead receive
# # certain log levels as errors.
# sentry:
# type: sentry
# level: !php/const Monolog\Logger::ERROR
# hub_id: Sentry\State\HubInterface
# fill_extra_context: true # Enables sending monolog context to Sentry
# process_psr_3_messages: false # Disables the resolution of PSR-3 placeholders
#
# # Use this for structured log integration
# sentry_logs:
# type: service
# id: Sentry\SentryBundle\Monolog\LogsHandler
#
# services:
# Sentry\SentryBundle\Monolog\LogsHandler:
# arguments:
# - !php/const Monolog\Logger::INFO

View File

@@ -0,0 +1,7 @@
services:
stripe.client:
class: 'Stripe\StripeClient'
arguments:
- '%env(STRIPE_SECRET_KEY)%'
Stripe\StripeClient: '@stripe.client'

View File

@@ -0,0 +1,5 @@
framework:
default_locale: fr
translator:
default_path: '%kernel.project_dir%/translations'
providers:

View File

@@ -0,0 +1,7 @@
twig:
file_name_pattern: '*.twig'
form_themes:
- 'form_tailwind.twig'
when@test:
twig:
strict_variables: true

View File

@@ -0,0 +1,11 @@
framework:
validation:
# Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
#auto_mapping:
# App\Entity\: []
when@test:
framework:
validation:
not_compromised_password: false

View File

@@ -0,0 +1,96 @@
vich_uploader:
db_driver: orm
mappings:
avatar:
uri_prefix: /storage/avatar
upload_destination: '%kernel.project_dir%/public/storage/avatar'
namer: App\VichUploader\Namer\Account\AvatarName # Replaced namer
directory_namer: App\VichUploader\DirectoryNamer\Account\AvatarName
inject_on_load: true
delete_on_update: true
delete_on_remove: true
members:
uri_prefix: /storage/members
upload_destination: '%kernel.project_dir%/public/storage/members'
namer: App\VichUploader\Namer\Account\AvatarName # Replaced namer
directory_namer: App\VichUploader\DirectoryNamer\Account\AvatarName
inject_on_load: true
delete_on_update: true
delete_on_remove: true
members_page_avatar:
uri_prefix: /storage/members_page_avatar
upload_destination: '%kernel.project_dir%/public/storage/members_page_avatar'
namer: Vich\UploaderBundle\Naming\UniqidNamer
inject_on_load: true
delete_on_update: true
delete_on_remove: true
members_page_cosplay:
uri_prefix: /storage/members_page_avatar
upload_destination: '%kernel.project_dir%/public/storage/members_page_avatar'
namer: Vich\UploaderBundle\Naming\UniqidNamer
inject_on_load: true
delete_on_update: true
delete_on_remove: true
members_page_cosplay_details:
uri_prefix: /storage/members_page_cosplay_details
upload_destination: '%kernel.project_dir%/public/storage/members_page_cosplay_details'
namer: Vich\UploaderBundle\Naming\UniqidNamer
inject_on_load: true
delete_on_update: true
delete_on_remove: true
ag:
uri_prefix: /storage/ag
upload_destination: '%kernel.project_dir%/public/storage/ag'
namer: Vich\UploaderBundle\Naming\UniqidNamer # Replaced namer
inject_on_load: true
delete_on_update: true
delete_on_remove: true
ag_adh:
uri_prefix: /storage/ag_adh
upload_destination: '%kernel.project_dir%/public/storage/ag_adh'
namer: Vich\UploaderBundle\Naming\UniqidNamer # Replaced namer
inject_on_load: true
delete_on_update: true
delete_on_remove: true
product:
uri_prefix: /storage/product
upload_destination: '%kernel.project_dir%/public/storage/product'
namer: Vich\UploaderBundle\Naming\UniqidNamer # Replaced namer
inject_on_load: true
delete_on_update: true
delete_on_remove: true
events:
uri_prefix: /storage/events
upload_destination: '%kernel.project_dir%/public/storage/events'
namer: Vich\UploaderBundle\Naming\UniqidNamer # Replaced namer
inject_on_load: true
delete_on_update: true
delete_on_remove: true
event_picture:
uri_prefix: /event_picture/events
upload_destination: '%kernel.project_dir%/public/storage/event_picture'
namer: Vich\UploaderBundle\Naming\UniqidNamer # Replaced namer
directory_namer: App\VichUploader\DirectoryNamer\EventName
inject_on_load: true
delete_on_update: true
delete_on_remove: true
epage_avatar:
uri_prefix: /storage/epage/
upload_destination: '%kernel.project_dir%/public/storage/epage'
namer: App\VichUploader\Namer\Epage\AvatarNamer # Replaced namer
directory_namer: App\VichUploader\DirectoryNamer\Epage\DirectoryNamer
inject_on_load: true
delete_on_update: true
delete_on_remove: true
fiche:
uri_prefix: /storage/fiche_candidat
upload_destination: '%kernel.project_dir%/public/storage/fiche_candidat'
namer: Vich\UploaderBundle\Naming\UniqidNamer # Replaced namer
inject_on_load: true
delete_on_update: true
delete_on_remove: true
#mappings:
# products:
# uri_prefix: /images/products
# upload_destination: '%kernel.project_dir%/public/images/products'
# namer: Vich\UploaderBundle\Naming\SmartUniqueNamer

View File

@@ -0,0 +1,13 @@
when@dev:
web_profiler:
toolbar: true
intercept_redirects: false
framework:
profiler:
collect_serializer_data: true
when@test:
framework:
profiler:
collect: false
collect_serializer_data: true

View File

@@ -0,0 +1,22 @@
nelmio_security:
csp:
enforce:
script-src:
- 'self'
- 'nonce'
- 'https://static.cloudflareinsights.com'
- 'https://challenges.cloudflare.com'
# Restreindre les soumissions de formulaires à notre domaine
# et aux redirections OAuth des plateformes de partage social
form-action:
- 'self'
- 'https://www.facebook.com'
- 'https://x.com'
- 'https://twitter.com'
# Autoriser navigator.share() (Web Share API) et clipboard API
# — les deux sont des APIs navigateur natives, pas des appels réseau externes
# Ce bloc est présent pour documentation et futures intégrations
connect-src:
- 'self'

8
config/packages/pwa.yaml Normal file
View File

@@ -0,0 +1,8 @@
# Documentation available at https://pwa.spomky-labs.com/
## Please add the favicons, manifest or serviceworker sections depending on your needs.
pwa:
asset_compiler: false
image_processor: 'pwa.image_processor.gd'
favicons:
enabled: true
src: '%kernel.project_dir%/public/favicon.png'

View File

@@ -0,0 +1,20 @@
scheb_two_factor:
security_tokens:
- Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
- Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
email:
enabled: true
sender_email: 'contact@e-cosplay.fr'
sender_name: 'CRM Ecosplay'
digits: 6
template: 'security/2fa_email.html.twig'
mailer: App\Security\TwoFactorCodeMailer
google:
enabled: true
issuer: 'CRM Ecosplay'
template: 'security/2fa_google.html.twig'
backup_codes:
enabled: true

View File

@@ -5,27 +5,53 @@ security:
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
users_in_memory: { memory: null }
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
# Ensure dev tools and static assets are always allowed
pattern: ^/(_profiler|_wdt|assets|build)/
security: false
webhooks:
pattern: ^/webhooks/
security: false
main:
lazy: true
provider: users_in_memory
provider: app_user_provider
entry_point: form_login
custom_authenticators:
- App\Security\KeycloakAuthenticator
form_login:
login_path: app_home
check_path: app_home
username_parameter: _username
password_parameter: _password
success_handler: App\Security\LoginSuccessHandler
logout:
path: app_logout
two_factor:
auth_form_path: 2fa_login
check_path: 2fa_login_check
default_target_path: app_home
prepare_on_login: true
prepare_on_access_denied: true
enable_csrf: true
# Activate different ways to authenticate:
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
role_hierarchy:
ROLE_CUSTOMER: ROLE_USER
ROLE_REVENDEUR: ROLE_CUSTOMER
ROLE_MEMBER: ROLE_USER
ROLE_EMPLOYE: ROLE_MEMBER
ROLE_ROOT: [ROLE_EMPLOYE, ROLE_REVENDEUR]
# Note: Only the *first* matching rule is applied
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
- { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
- { path: ^/admin, roles: ROLE_EMPLOYE }
- { path: ^/espace-client, roles: ROLE_CUSTOMER }
- { path: ^/espace-prestataire, roles: ROLE_REVENDEUR }
when@test:
security:

View File

@@ -0,0 +1,7 @@
services:
stripe.client:
class: 'Stripe\StripeClient'
arguments:
- '%env(STRIPE_SECRET_KEY)%'
Stripe\StripeClient: '@stripe.client'

View File

@@ -1,4 +0,0 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
csrf_protection:
check_header: true

View File

@@ -0,0 +1,8 @@
vich_uploader:
db_driver: orm
mappings:
user_avatar:
uri_prefix: /uploads/avatars
upload_destination: '%kernel.project_dir%/public/uploads/avatars'
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer

View File

@@ -622,7 +622,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* }>,
* },
* rate_limiter?: bool|array{ // Rate limiter configuration
* enabled?: bool|Param, // Default: false
* enabled?: bool|Param, // Default: true
* limiters?: array<string, array{ // Default: []
* lock_factory?: scalar|Param|null, // The service ID of the lock factory used by this limiter (or null to disable locking). // Default: "auto"
* cache_pool?: scalar|Param|null, // The cache pool to use for storing the current limiter state. // Default: "cache.rate_limiter"
@@ -938,20 +938,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* intercept_redirects?: bool|Param, // Default: false
* excluded_ajax_paths?: scalar|Param|null, // Default: "^/((index|app(_[\\w]+)?)\\.php/)?_wdt"
* }
* @psalm-type StimulusConfig = array{
* controller_paths?: list<scalar|Param|null>,
* controllers_json?: scalar|Param|null, // Default: "%kernel.project_dir%/assets/controllers.json"
* }
* @psalm-type TurboConfig = array{
* broadcast?: bool|array{
* enabled?: bool|Param, // Default: true
* entity_template_prefixes?: list<scalar|Param|null>,
* doctrine_orm?: bool|array{ // Enable the Doctrine ORM integration
* enabled?: bool|Param, // Default: true
* },
* },
* default_transport?: scalar|Param|null, // Default: "default"
* }
* @psalm-type TwigExtraConfig = array{
* cache?: bool|array{
* enabled?: bool|Param, // Default: false
@@ -966,10 +952,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* enabled?: bool|Param, // Default: false
* },
* cssinliner?: bool|array{
* enabled?: bool|Param, // Default: false
* enabled?: bool|Param, // Default: true
* },
* inky?: bool|array{
* enabled?: bool|Param, // Default: false
* enabled?: bool|Param, // Default: true
* },
* string?: bool|array{
* enabled?: bool|Param, // Default: false
@@ -1291,6 +1277,28 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* always_remember_me?: bool|Param, // Default: false
* remember_me_parameter?: scalar|Param|null, // Default: "_remember_me"
* },
* two_factor?: array{
* check_path?: scalar|Param|null, // Default: "/2fa_check"
* post_only?: bool|Param, // Default: true
* auth_form_path?: scalar|Param|null, // Default: "/2fa"
* always_use_default_target_path?: bool|Param, // Default: false
* default_target_path?: scalar|Param|null, // Default: "/"
* success_handler?: scalar|Param|null, // Default: null
* failure_handler?: scalar|Param|null, // Default: null
* authentication_required_handler?: scalar|Param|null, // Default: null
* auth_code_parameter_name?: scalar|Param|null, // Default: "_auth_code"
* trusted_parameter_name?: scalar|Param|null, // Default: "_trusted"
* remember_me_sets_trusted?: scalar|Param|null, // Default: false
* multi_factor?: bool|Param, // Default: false
* prepare_on_login?: bool|Param, // Default: false
* prepare_on_access_denied?: bool|Param, // Default: false
* enable_csrf?: scalar|Param|null, // Default: false
* csrf_parameter?: scalar|Param|null, // Default: "_csrf_token"
* csrf_token_id?: scalar|Param|null, // Default: "two_factor"
* csrf_header?: scalar|Param|null, // Default: null
* csrf_token_manager?: scalar|Param|null, // Default: "scheb_two_factor.csrf_token_manager"
* provider?: scalar|Param|null, // Default: null
* },
* }>,
* access_control?: list<array{ // Default: []
* request_matcher?: scalar|Param|null, // Default: null
@@ -1455,6 +1463,818 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* generate_final_classes?: bool|Param, // Default: true
* generate_final_entities?: bool|Param, // Default: false
* }
* @psalm-type EndroidQrCodeConfig = array<string, mixed>
* @psalm-type KnpPaginatorConfig = array{
* default_options?: array{
* sort_field_name?: scalar|Param|null, // Default: "sort"
* sort_direction_name?: scalar|Param|null, // Default: "direction"
* filter_field_name?: scalar|Param|null, // Default: "filterField"
* filter_value_name?: scalar|Param|null, // Default: "filterValue"
* page_name?: scalar|Param|null, // Default: "page"
* distinct?: bool|Param, // Default: true
* page_out_of_range?: scalar|Param|null, // Default: "ignore"
* default_limit?: scalar|Param|null, // Default: 10
* },
* template?: array{
* pagination?: scalar|Param|null, // Default: "@KnpPaginator/Pagination/sliding.html.twig"
* rel_links?: scalar|Param|null, // Default: "@KnpPaginator/Pagination/rel_links.html.twig"
* filtration?: scalar|Param|null, // Default: "@KnpPaginator/Pagination/filtration.html.twig"
* sortable?: scalar|Param|null, // Default: "@KnpPaginator/Pagination/sortable_link.html.twig"
* },
* page_range?: scalar|Param|null, // Default: 5
* page_limit?: scalar|Param|null, // Default: null
* convert_exception?: bool|Param, // Default: false
* remove_first_page_param?: bool|Param, // Default: false
* }
* @psalm-type KnpuOauth2ClientConfig = array{
* http_client?: scalar|Param|null, // Service id of HTTP client to use (must implement GuzzleHttp\ClientInterface) // Default: null
* http_client_options?: array{
* timeout?: int|Param,
* proxy?: scalar|Param|null,
* verify?: bool|Param, // Use only with proxy option set
* },
* clients?: array<string, array<string, mixed>>,
* }
* @psalm-type FlysystemConfig = array{
* storages?: array<string, array{ // Default: []
* adapter: scalar|Param|null,
* options?: list<mixed>,
* visibility?: scalar|Param|null, // Default: null
* directory_visibility?: scalar|Param|null, // Default: null
* retain_visibility?: bool|Param|null, // Default: null
* case_sensitive?: bool|Param, // Default: true
* disable_asserts?: bool|Param, // Default: false
* public_url?: list<scalar|Param|null>,
* path_normalizer?: scalar|Param|null, // Default: null
* public_url_generator?: scalar|Param|null, // Default: null
* temporary_url_generator?: scalar|Param|null, // Default: null
* read_only?: bool|Param, // Default: false
* }>,
* }
* @psalm-type LiipImagineConfig = array{
* resolvers?: array<string, array{ // Default: []
* web_path?: array{
* web_root?: scalar|Param|null, // Default: "%kernel.project_dir%/public"
* cache_prefix?: scalar|Param|null, // Default: "media/cache"
* },
* aws_s3?: array{
* bucket?: scalar|Param|null,
* cache?: scalar|Param|null, // Default: false
* use_psr_cache?: bool|Param, // Default: false
* acl?: scalar|Param|null, // Default: "public-read"
* cache_prefix?: scalar|Param|null, // Default: ""
* client_id?: scalar|Param|null, // Default: null
* client_config?: list<mixed>,
* get_options?: array<string, scalar|Param|null>,
* put_options?: array<string, scalar|Param|null>,
* proxies?: array<string, scalar|Param|null>,
* },
* flysystem?: array{
* filesystem_service?: scalar|Param|null,
* cache_prefix?: scalar|Param|null, // Default: ""
* root_url?: scalar|Param|null,
* visibility?: "public"|"private"|"noPredefinedVisibility"|Param, // Default: "public"
* },
* }>,
* loaders?: array<string, array{ // Default: []
* stream?: array{
* wrapper?: scalar|Param|null,
* context?: scalar|Param|null, // Default: null
* },
* filesystem?: array{
* locator?: "filesystem"|"filesystem_insecure"|Param, // Using the "filesystem_insecure" locator is not recommended due to a less secure resolver mechanism, but is provided for those using heavily symlinked projects. // Default: "filesystem"
* data_root?: list<scalar|Param|null>,
* allow_unresolvable_data_roots?: bool|Param, // Default: false
* bundle_resources?: array{
* enabled?: bool|Param, // Default: false
* access_control_type?: "blacklist"|"whitelist"|Param, // Sets the access control method applied to bundle names in "access_control_list" into a blacklist or whitelist. // Default: "blacklist"
* access_control_list?: list<scalar|Param|null>,
* },
* },
* flysystem?: array{
* filesystem_service?: scalar|Param|null,
* },
* asset_mapper?: array<mixed>,
* chain?: array{
* loaders?: list<scalar|Param|null>,
* },
* }>,
* driver?: scalar|Param|null, // Default: "gd"
* cache?: scalar|Param|null, // Default: "default"
* cache_base_path?: scalar|Param|null, // Default: ""
* data_loader?: scalar|Param|null, // Default: "default"
* default_image?: scalar|Param|null, // Default: null
* default_filter_set_settings?: array{
* quality?: scalar|Param|null, // Default: 100
* jpeg_quality?: scalar|Param|null, // Default: null
* png_compression_level?: scalar|Param|null, // Default: null
* png_compression_filter?: scalar|Param|null, // Default: null
* format?: scalar|Param|null, // Default: null
* animated?: bool|Param, // Default: false
* cache?: scalar|Param|null, // Default: null
* data_loader?: scalar|Param|null, // Default: null
* default_image?: scalar|Param|null, // Default: null
* filters?: array<string, array<string, mixed>>,
* post_processors?: array<string, array<string, mixed>>,
* },
* controller?: array{
* filter_action?: scalar|Param|null, // Default: "Liip\\ImagineBundle\\Controller\\ImagineController::filterAction"
* filter_runtime_action?: scalar|Param|null, // Default: "Liip\\ImagineBundle\\Controller\\ImagineController::filterRuntimeAction"
* redirect_response_code?: int|Param, // Default: 302
* },
* filter_sets?: array<string, array{ // Default: []
* quality?: scalar|Param|null,
* jpeg_quality?: scalar|Param|null,
* png_compression_level?: scalar|Param|null,
* png_compression_filter?: scalar|Param|null,
* format?: scalar|Param|null,
* animated?: bool|Param,
* cache?: scalar|Param|null,
* data_loader?: scalar|Param|null,
* default_image?: scalar|Param|null,
* filters?: array<string, array<string, mixed>>,
* post_processors?: array<string, array<string, mixed>>,
* }>,
* twig?: array{
* mode?: "none"|"lazy"|"legacy"|Param, // Twig mode: none/lazy/legacy (default) // Default: "legacy"
* assets_version?: scalar|Param|null, // Default: null
* },
* enqueue?: bool|Param, // Enables integration with enqueue if set true. Allows resolve image caches in background by sending messages to MQ. // Default: false
* messenger?: bool|array{ // Enables integration with symfony/messenger if set true. Warmup image caches in background by sending messages to MQ.
* enabled?: bool|Param, // Default: false
* },
* templating?: bool|Param, // Enables integration with symfony/templating component // Default: true
* webp?: array{
* generate?: bool|Param, // Default: false
* quality?: int|Param, // Default: 100
* cache?: scalar|Param|null, // Default: null
* data_loader?: scalar|Param|null, // Default: null
* post_processors?: array<string, array<string, mixed>>,
* },
* }
* @psalm-type NelmioSecurityConfig = array{
* signed_cookie?: array{
* names?: list<scalar|Param|null>,
* secret?: scalar|Param|null, // Default: "%kernel.secret%"
* hash_algo?: scalar|Param|null,
* legacy_hash_algo?: scalar|Param|null, // Fallback algorithm to allow for frictionless hash algorithm upgrades. Use with caution and as a temporary measure as it allows for downgrade attacks. // Default: null
* separator?: scalar|Param|null, // Default: "."
* },
* clickjacking?: array{
* hosts?: list<scalar|Param|null>,
* paths?: array<string, array{ // Default: {"^/.*":{"header":"DENY"}}
* header?: scalar|Param|null, // Default: "DENY"
* }>,
* content_types?: list<scalar|Param|null>,
* },
* external_redirects?: array{
* abort?: bool|Param, // Default: false
* override?: scalar|Param|null, // Default: null
* forward_as?: scalar|Param|null, // Default: null
* log?: bool|Param, // Default: false
* allow_list?: list<scalar|Param|null>,
* },
* flexible_ssl?: bool|array{
* enabled?: bool|Param, // Default: false
* cookie_name?: scalar|Param|null, // Default: "auth"
* unsecured_logout?: bool|Param, // Default: false
* },
* forced_ssl?: bool|array{
* enabled?: bool|Param, // Default: false
* hsts_max_age?: scalar|Param|null, // Default: null
* hsts_subdomains?: bool|Param, // Default: false
* hsts_preload?: bool|Param, // Default: false
* allow_list?: list<scalar|Param|null>,
* hosts?: list<scalar|Param|null>,
* redirect_status_code?: scalar|Param|null, // Default: 302
* },
* content_type?: array{
* nosniff?: bool|Param, // Default: false
* },
* xss_protection?: array{ // Deprecated: The "xss_protection" option is deprecated, use Content Security Policy without allowing "unsafe-inline" scripts instead.
* enabled?: bool|Param, // Default: false
* mode_block?: bool|Param, // Default: false
* report_uri?: scalar|Param|null, // Default: null
* },
* csp?: bool|array{
* enabled?: bool|Param, // Default: true
* request_matcher?: scalar|Param|null, // Default: null
* hosts?: list<scalar|Param|null>,
* content_types?: list<scalar|Param|null>,
* report_endpoint?: array{
* log_channel?: scalar|Param|null, // Default: null
* log_formatter?: scalar|Param|null, // Default: "nelmio_security.csp_report.log_formatter"
* log_level?: "alert"|"critical"|"debug"|"emergency"|"error"|"info"|"notice"|"warning"|Param, // Default: "notice"
* filters?: array{
* domains?: bool|Param, // Default: true
* schemes?: bool|Param, // Default: true
* browser_bugs?: bool|Param, // Default: true
* injected_scripts?: bool|Param, // Default: true
* },
* dismiss?: list<list<"default-src"|"base-uri"|"block-all-mixed-content"|"child-src"|"connect-src"|"font-src"|"form-action"|"frame-ancestors"|"frame-src"|"img-src"|"manifest-src"|"media-src"|"object-src"|"plugin-types"|"script-src"|"style-src"|"upgrade-insecure-requests"|"report-uri"|"worker-src"|"prefetch-src"|"report-to"|"*"|Param>>,
* },
* compat_headers?: bool|Param, // Default: true
* report_logger_service?: scalar|Param|null, // Default: "logger"
* hash?: array{
* algorithm?: "sha256"|"sha384"|"sha512"|Param, // The algorithm to use for hashes // Default: "sha256"
* },
* report?: array{
* level1_fallback?: bool|Param, // Provides CSP Level 1 fallback when using hash or nonce (CSP level 2) by adding 'unsafe-inline' source. See https://www.w3.org/TR/CSP2/#directive-script-src and https://www.w3.org/TR/CSP2/#directive-style-src // Default: true
* browser_adaptive?: bool|array{ // Do not send directives that browser do not support
* enabled?: bool|Param, // Default: false
* parser?: scalar|Param|null, // Default: "nelmio_security.ua_parser.ua_php"
* },
* default-src?: list<scalar|Param|null>,
* base-uri?: list<scalar|Param|null>,
* block-all-mixed-content?: bool|Param, // Default: false
* child-src?: list<scalar|Param|null>,
* connect-src?: list<scalar|Param|null>,
* font-src?: list<scalar|Param|null>,
* form-action?: list<scalar|Param|null>,
* frame-ancestors?: list<scalar|Param|null>,
* frame-src?: list<scalar|Param|null>,
* img-src?: list<scalar|Param|null>,
* manifest-src?: list<scalar|Param|null>,
* media-src?: list<scalar|Param|null>,
* object-src?: list<scalar|Param|null>,
* plugin-types?: list<scalar|Param|null>,
* script-src?: list<scalar|Param|null>,
* style-src?: list<scalar|Param|null>,
* upgrade-insecure-requests?: bool|Param, // Default: false
* report-uri?: list<scalar|Param|null>,
* worker-src?: list<scalar|Param|null>,
* prefetch-src?: list<scalar|Param|null>,
* report-to?: scalar|Param|null,
* },
* enforce?: array{
* level1_fallback?: bool|Param, // Provides CSP Level 1 fallback when using hash or nonce (CSP level 2) by adding 'unsafe-inline' source. See https://www.w3.org/TR/CSP2/#directive-script-src and https://www.w3.org/TR/CSP2/#directive-style-src // Default: true
* browser_adaptive?: bool|array{ // Do not send directives that browser do not support
* enabled?: bool|Param, // Default: false
* parser?: scalar|Param|null, // Default: "nelmio_security.ua_parser.ua_php"
* },
* default-src?: list<scalar|Param|null>,
* base-uri?: list<scalar|Param|null>,
* block-all-mixed-content?: bool|Param, // Default: false
* child-src?: list<scalar|Param|null>,
* connect-src?: list<scalar|Param|null>,
* font-src?: list<scalar|Param|null>,
* form-action?: list<scalar|Param|null>,
* frame-ancestors?: list<scalar|Param|null>,
* frame-src?: list<scalar|Param|null>,
* img-src?: list<scalar|Param|null>,
* manifest-src?: list<scalar|Param|null>,
* media-src?: list<scalar|Param|null>,
* object-src?: list<scalar|Param|null>,
* plugin-types?: list<scalar|Param|null>,
* script-src?: list<scalar|Param|null>,
* style-src?: list<scalar|Param|null>,
* upgrade-insecure-requests?: bool|Param, // Default: false
* report-uri?: list<scalar|Param|null>,
* worker-src?: list<scalar|Param|null>,
* prefetch-src?: list<scalar|Param|null>,
* report-to?: scalar|Param|null,
* },
* },
* referrer_policy?: bool|array{
* enabled?: bool|Param, // Default: false
* policies?: list<scalar|Param|null>,
* },
* permissions_policy?: bool|array{
* enabled?: bool|Param, // Default: false
* policies?: array{
* accelerometer?: mixed, // Default: null
* ambient_light_sensor?: mixed, // Default: null
* attribution_reporting?: mixed, // Default: null
* autoplay?: mixed, // Default: null
* bluetooth?: mixed, // Default: null
* browsing_topics?: mixed, // Default: null
* camera?: mixed, // Default: null
* captured_surface_control?: mixed, // Default: null
* compute_pressure?: mixed, // Default: null
* cross_origin_isolated?: mixed, // Default: null
* deferred_fetch?: mixed, // Default: null
* deferred_fetch_minimal?: mixed, // Default: null
* display_capture?: mixed, // Default: null
* encrypted_media?: mixed, // Default: null
* fullscreen?: mixed, // Default: null
* gamepad?: mixed, // Default: null
* geolocation?: mixed, // Default: null
* gyroscope?: mixed, // Default: null
* hid?: mixed, // Default: null
* identity_credentials_get?: mixed, // Default: null
* idle_detection?: mixed, // Default: null
* interest_cohort?: mixed, // Default: null
* language_detector?: mixed, // Default: null
* local_fonts?: mixed, // Default: null
* magnetometer?: mixed, // Default: null
* microphone?: mixed, // Default: null
* midi?: mixed, // Default: null
* otp_credentials?: mixed, // Default: null
* payment?: mixed, // Default: null
* picture_in_picture?: mixed, // Default: null
* publickey_credentials_create?: mixed, // Default: null
* publickey_credentials_get?: mixed, // Default: null
* screen_wake_lock?: mixed, // Default: null
* serial?: mixed, // Default: null
* speaker_selection?: mixed, // Default: null
* storage_access?: mixed, // Default: null
* summarizer?: mixed, // Default: null
* translator?: mixed, // Default: null
* usb?: mixed, // Default: null
* web_share?: mixed, // Default: null
* window_management?: mixed, // Default: null
* xr_spatial_tracking?: mixed, // Default: null
* },
* },
* cross_origin_isolation?: bool|array{
* enabled?: bool|Param, // Default: false
* paths?: array<string, array{ // Default: []
* coep?: "unsafe-none"|"require-corp"|"credentialless"|Param, // Cross-Origin-Embedder-Policy (COEP) header value
* coop?: "unsafe-none"|"same-origin-allow-popups"|"same-origin"|"noopener-allow-popups"|Param, // Cross-Origin-Opener-Policy (COOP) header value
* corp?: "same-site"|"same-origin"|"cross-origin"|Param, // Cross-Origin-Resource-Policy (CORP) header value
* report_only?: bool|Param, // Use Report-Only headers instead of enforcing (applies to COEP and COOP only) // Default: false
* report_to?: scalar|Param|null, // Reporting endpoint name for violations (requires Reporting API configuration, applies to COEP and COOP only) // Default: null
* }>,
* },
* }
* @psalm-type PwaConfig = array{
* asset_compiler?: bool|Param, // When true, the assets will be compiled when the command "asset-map:compile" is run. // Default: true
* early_hints?: bool|array{ // Early Hints (HTTP 103) configuration. Requires a compatible server (FrankenPHP, Caddy).
* enabled?: bool|Param, // Default: false
* preload_manifest?: bool|Param, // Preload the PWA manifest file. // Default: true
* preload_serviceworker?: bool|Param, // Preload the service worker script. Disabled by default as SW registration is usually deferred. // Default: false
* preconnect_workbox_cdn?: bool|Param, // Preconnect to Workbox CDN when using CDN mode. // Default: true
* },
* favicons?: bool|array{
* enabled?: bool|Param, // Default: false
* default?: array{ // The favicon source and parameters. When used with "dark", this favicon will become the light version.
* src?: scalar|Param|null, // The path to the icon. Can be served by Asset Mapper, an absolute path or a Symfony UX Icon (if the bundle is installed).
* background_color?: scalar|Param|null, // The background color of the application. If this value is not defined and that of the Manifest section is, the value of the latter will be used. // Default: null
* border_radius?: int|Param, // The border radius of the icon. // Default: null
* image_scale?: int|Param, // The scale of the icon. // Default: null
* svg_attr?: array<string, mixed>,
* },
* dark?: array{ // The favicon source and parameters for the dark theme. Should only be used with "default".
* src?: scalar|Param|null, // The path to the icon. Can be served by Asset Mapper, an absolute path or a Symfony UX Icon (if the bundle is installed).
* background_color?: scalar|Param|null, // The background color of the application. If this value is not defined and that of the Manifest section is, the value of the latter will be used. // Default: null
* border_radius?: int|Param, // The border radius of the icon. // Default: null
* image_scale?: int|Param, // The scale of the icon. // Default: null
* svg_attr?: array<string, mixed>,
* },
* src?: scalar|Param|null, // Deprecated: The "src" configuration key is deprecated. Use the "default.src" configuration key instead. // The source of the favicon. Shall be a SVG or large PNG. // Default: null
* src_dark?: scalar|Param|null, // Deprecated: The "src_dark" configuration key is deprecated. Use the "dark.src" configuration key instead. // The source of the favicon in dark mode. Shall be a SVG or large PNG. // Default: null
* background_color?: scalar|Param|null, // Deprecated: The "background_color" configuration key is deprecated. Use the "default.background_color" configuration key instead. // The background color of the icon. // Default: null
* background_color_dark?: scalar|Param|null, // Deprecated: The "background_color_dark" configuration key is deprecated. Use the "dark.background_color" configuration key instead. // The background color of the icon in dark mode. // Default: null
* safari_pinned_tab_color?: scalar|Param|null, // The color of the Safari pinned tab. Requires "use_silhouette" to be set to "true". // Default: null
* tile_color?: scalar|Param|null, // The color of the tile for Windows 8+. // Default: null
* border_radius?: int|Param, // Deprecated: The "border_radius" configuration key is deprecated. Use the "default.border_radius" or "dark.border_radius" configuration key instead. // The border radius of the icon. // Default: null
* image_scale?: int|Param, // Deprecated: The "image_scale" configuration key is deprecated. Use the "default.image_scale" or "dark.image_scale" configuration key instead. // The scale of the icon. // Default: null
* low_resolution?: bool|Param, // Include low resolution icons. // Default: false
* use_silhouette?: bool|Param|null, // Use only the silhouette of the icon. Applicable for macOS Safari and Windows 8+. Requires potrace to be installed. // Default: null
* use_start_image?: bool|Param, // Use the icon as a start image for the iOS splash screen. // Default: true
* svg_color?: scalar|Param|null, // When the asset is a SVG file, replaces the currentColor attribute with this color. // Default: "#000"
* monochrome?: bool|Param, // Use a monochrome icon. // Default: false
* potrace?: scalar|Param|null, // The path to the potrace binary. // Default: "potrace"
* },
* image_processor?: scalar|Param|null, // The image processor to use to generate the icons of different sizes. // Default: null
* logger?: scalar|Param|null, // The logger service to use. If not set, the default logger will be used. // Default: null
* manifest?: bool|array{
* enabled?: bool|Param, // Default: false
* public_url?: scalar|Param|null, // The public URL of the manifest file. // Default: "/site.webmanifest"
* use_credentials?: bool|Param, // Indicates whether the manifest should be fetched with credentials. // Default: true
* background_color?: scalar|Param|null, // The background color of the application. It should match the background-color CSS property in the sites stylesheet for a smooth transition between launching the web application and loading the site's content.
* categories?: list<scalar|Param|null>,
* description?: scalar|Param|null, // The description of the application.
* display?: scalar|Param|null, // The display mode of the application.
* display_override?: list<scalar|Param|null>,
* id?: scalar|Param|null, // A string that represents the identity of the web application.
* orientation?: scalar|Param|null, // The orientation of the application.
* dir?: scalar|Param|null, // The direction of the application.
* lang?: scalar|Param|null, // The language of the application.
* name?: scalar|Param|null, // The name of the application.
* short_name?: scalar|Param|null, // The short name of the application.
* scope?: scalar|Param|null, // The scope of the application.
* start_url?: string|array{ // The start URL of the application.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* theme_color?: scalar|Param|null, // The theme color of the application. If a dark theme color is specified, the theme color will be used for the light theme.
* dark_theme_color?: scalar|Param|null, // The dark theme color of the application.
* edge_side_panel?: array{ // Specifies whether or not your app supports the side panel view in Microsoft Edge.
* preferred_width?: int|Param, // Specifies the preferred width of the side panel view in Microsoft Edge.
* },
* iarc_rating_id?: scalar|Param|null, // Specifies the International Age Rating Coalition (IARC) rating ID for the app. See https://www.globalratings.com/how-iarc-works.aspx for more information.
* scope_extensions?: list<array{ // Default: []
* type?: scalar|Param|null, // Specifies the type of scope extension. This is currently always origin (default), but future extensions may add other types. // Default: "origin"
* origin?: scalar|Param|null, // Specifies the origin pattern to associate with.
* }>,
* handle_links?: scalar|Param|null, // Specifies the default link handling for the web app.
* note_taking?: array{ // The note-taking capabilities of the application.
* note_taking_url?: string|array{ // The URL to the note-taking service.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* },
* icons?: list<string|array{ // Default: []
* src?: scalar|Param|null, // The path to the icon. Can be served by Asset Mapper, an absolute path or a Symfony UX Icon (if the bundle is installed).
* sizes?: list<int|Param>,
* background_color?: scalar|Param|null, // The background color of the application. If this value is not defined and that of the Manifest section is, the value of the latter will be used. // Default: null
* border_radius?: int|Param, // The border radius of the icon. // Default: null
* image_scale?: int|Param, // The scale of the icon. // Default: null
* type?: scalar|Param|null, // The icon mime type.
* format?: scalar|Param|null, // The icon format. When set, the "type" option is ignored and the image will be converted.
* purpose?: scalar|Param|null, // The purpose of the icon.
* svg_attr?: array<string, mixed>,
* }>,
* screenshots?: list<string|array{ // Default: []
* src?: scalar|Param|null, // The path to the screenshot. Can be served by Asset Mapper.
* height?: scalar|Param|null, // Default: null
* width?: scalar|Param|null, // Default: null
* form_factor?: scalar|Param|null, // The form factor of the screenshot. Will guess the form factor if not set.
* label?: scalar|Param|null, // The label of the screenshot.
* platform?: scalar|Param|null, // The platform of the screenshot.
* format?: scalar|Param|null, // The format of the screenshot. Will convert the file if set.
* reference?: scalar|Param|null, // The URL of the screenshot. Only for reference and not used by the bundle. // Default: null
* }>,
* file_handlers?: list<array{ // Default: []
* action?: string|array{ // The action to take.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* accept?: array<string, list<scalar|Param|null>>,
* }>,
* launch_handler?: array{ // The launch handler of the application.
* client_mode?: list<scalar|Param|null>,
* },
* protocol_handlers?: list<array{ // Default: []
* protocol?: scalar|Param|null, // The protocol of the handler.
* placeholder?: scalar|Param|null, // The placeholder of the handler. Will be replaced by "xxx=%s". // Default: null
* url?: string|array{ // The URL of the handler.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* }>,
* prefer_related_applications?: bool|Param, // prefer related native applications (instead of this application) // Default: false
* related_applications?: list<array{ // Default: []
* platform?: scalar|Param|null, // The platform of the application.
* url?: string|array{ // The URL of the application.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* id?: scalar|Param|null, // The ID of the application.
* }>,
* shortcuts?: list<array{ // Default: []
* name?: scalar|Param|null, // The name of the shortcut.
* short_name?: scalar|Param|null, // The short name of the shortcut.
* description?: scalar|Param|null, // The description of the shortcut.
* url?: string|array{ // The URL of the shortcut.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* icons?: list<string|array{ // Default: []
* src?: scalar|Param|null, // The path to the icon. Can be served by Asset Mapper, an absolute path or a Symfony UX Icon (if the bundle is installed).
* sizes?: list<int|Param>,
* background_color?: scalar|Param|null, // The background color of the application. If this value is not defined and that of the Manifest section is, the value of the latter will be used. // Default: null
* border_radius?: int|Param, // The border radius of the icon. // Default: null
* image_scale?: int|Param, // The scale of the icon. // Default: null
* type?: scalar|Param|null, // The icon mime type.
* format?: scalar|Param|null, // The icon format. When set, the "type" option is ignored and the image will be converted.
* purpose?: scalar|Param|null, // The purpose of the icon.
* svg_attr?: array<string, mixed>,
* }>,
* }>,
* share_target?: array{ // The share target of the application.
* action?: string|array{ // The action of the share target.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* method?: scalar|Param|null, // The method of the share target.
* enctype?: scalar|Param|null, // The enctype of the share target. Ignored if method is GET.
* params?: array{ // The parameters of the share target.
* title?: scalar|Param|null, // The title of the share target.
* text?: scalar|Param|null, // The text of the share target.
* url?: scalar|Param|null, // The URL of the share target.
* files?: list<array{ // Default: []
* name?: scalar|Param|null, // The name of the file parameter.
* accept?: list<scalar|Param|null>,
* }>,
* },
* },
* widgets?: list<array{ // Default: []
* name?: scalar|Param|null, // The title of the widget, presented to users.
* short_name?: scalar|Param|null, // An alternative short version of the name.
* description?: scalar|Param|null, // The description of the widget.
* icons?: list<string|array{ // Default: []
* src?: scalar|Param|null, // The path to the icon. Can be served by Asset Mapper, an absolute path or a Symfony UX Icon (if the bundle is installed).
* sizes?: list<int|Param>,
* background_color?: scalar|Param|null, // The background color of the application. If this value is not defined and that of the Manifest section is, the value of the latter will be used. // Default: null
* border_radius?: int|Param, // The border radius of the icon. // Default: null
* image_scale?: int|Param, // The scale of the icon. // Default: null
* type?: scalar|Param|null, // The icon mime type.
* format?: scalar|Param|null, // The icon format. When set, the "type" option is ignored and the image will be converted.
* purpose?: scalar|Param|null, // The purpose of the icon.
* svg_attr?: array<string, mixed>,
* }>,
* screenshots?: list<string|array{ // Default: []
* src?: scalar|Param|null, // The path to the screenshot. Can be served by Asset Mapper.
* height?: scalar|Param|null, // Default: null
* width?: scalar|Param|null, // Default: null
* form_factor?: scalar|Param|null, // The form factor of the screenshot. Will guess the form factor if not set.
* label?: scalar|Param|null, // The label of the screenshot.
* platform?: scalar|Param|null, // The platform of the screenshot.
* format?: scalar|Param|null, // The format of the screenshot. Will convert the file if set.
* reference?: scalar|Param|null, // The URL of the screenshot. Only for reference and not used by the bundle. // Default: null
* }>,
* tag?: scalar|Param|null, // A string used to reference the widget in the PWA service worker.
* template?: scalar|Param|null, // The template to use to display the widget in the operating system widgets dashboard. Note: this property is currently only informational and not used. See ms_ac_template below.
* ms_ac_template?: string|array{ // The URL of the custom Adaptive Cards template to use to display the widget in the operating system widgets dashboard.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* data?: string|array{ // The URL where the data to fill the template with can be found. If present, this URL is required to return valid JSON.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* type?: scalar|Param|null, // The MIME type for the widget data.
* auth?: bool|Param, // A boolean indicating if the widget requires authentication.
* update?: int|Param, // The frequency, in seconds, at which the widget will be updated. Code in your service worker must perform the updating; the widget is not updated automatically. See Access widget instances at runtime.
* multiple?: bool|Param, // A boolean indicating whether to allow multiple instances of the widget. Defaults to true. // Default: true
* }>,
* },
* path_type_reference?: int|Param, // Deprecated: The "path_type_reference" configuration key is deprecated. Use the "path_type_reference" of URL nodes instead. // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* resource_hints?: bool|array{ // Resource Hints configuration for preconnect, dns-prefetch, and preload.
* enabled?: bool|Param, // Default: false
* auto_preconnect?: bool|Param, // Automatically add preconnect hints for detected external origins (Workbox CDN, Google Fonts). // Default: true
* preconnect?: list<scalar|Param|null>,
* dns_prefetch?: list<scalar|Param|null>,
* preload?: list<array{ // Default: []
* href?: scalar|Param|null, // The URL or path to preload.
* as?: "script"|"style"|"font"|"image"|"fetch"|"document"|"audio"|"video"|"track"|"worker"|Param, // The resource type.
* type?: scalar|Param|null, // The MIME type of the resource. // Default: null
* crossorigin?: "anonymous"|"use-credentials"|Param, // The crossorigin attribute value. Required for fonts. // Default: null
* fetchpriority?: "high"|"low"|"auto"|Param, // The fetch priority hint. // Default: null
* media?: scalar|Param|null, // Media query for responsive preloading. // Default: null
* }>,
* },
* serviceworker?: bool|string|array{
* enabled?: bool|Param, // Default: false
* src?: scalar|Param|null, // The path to the service worker source file. Can be served by Asset Mapper.
* dest?: scalar|Param|null, // The public URL to the service worker. // Default: "/sw.js"
* skip_waiting?: bool|Param, // Whether to skip waiting for the service worker to be activated. // Default: false
* scope?: scalar|Param|null, // The scope of the service worker. // Default: "/"
* use_cache?: bool|Param, // Whether the service worker should use the cache. // Default: true
* workbox?: bool|array{ // The configuration of the workbox.
* enabled?: bool|Param, // Default: true
* use_cdn?: bool|Param, // Deprecated: The "use_cdn" option is deprecated and will be removed in 2.0.0. use "config.use_cdn" instead. // Whether to use the local workbox or the CDN. // Default: false
* google_fonts?: bool|array{
* enabled?: bool|Param, // Default: true
* cache_prefix?: scalar|Param|null, // The cache prefix for the Google fonts. // Default: null
* max_age?: scalar|Param|null, // The maximum age of the Google fonts cache (in seconds). // Default: null
* max_entries?: int|Param, // The maximum number of entries in the Google fonts cache. // Default: null
* },
* cache_manifest?: bool|Param, // Whether to cache the manifest file. // Default: true
* version?: scalar|Param|null, // Deprecated: The "version" option is deprecated and will be removed in 2.0.0. use "config.version" instead. // The version of workbox. When using local files, the version shall be "7.0.0." // Default: "7.3.0"
* workbox_public_url?: scalar|Param|null, // Deprecated: The "workbox_public_url" option is deprecated and will be removed in 2.0.0. use "config.workbox_public_url" instead. // The public path to the local workbox. Only used if use_cdn is false. // Default: "/workbox"
* idb_public_url?: scalar|Param|null, // The public path to the local IndexDB. Only used if use_cdn is false. // Default: "/idb"
* workbox_import_placeholder?: scalar|Param|null, // Deprecated: The "workbox_import_placeholder" option is deprecated and will be removed in 2.0.0. No replacement. // The placeholder for the workbox import. Will be replaced by the workbox import. // Default: "//WORKBOX_IMPORT_PLACEHOLDER"
* standard_rules_placeholder?: scalar|Param|null, // Deprecated: The "standard_rules_placeholder" option is deprecated and will be removed in 2.0.0. No replacement. // The placeholder for the standard rules. Will be replaced by caching strategies. // Default: "//STANDARD_RULES_PLACEHOLDER"
* offline_fallback_placeholder?: scalar|Param|null, // Deprecated: The "offline_fallback_placeholder" option is deprecated and will be removed in 2.0.0. No replacement. // The placeholder for the offline fallback. Will be replaced by the URL. // Default: "//OFFLINE_FALLBACK_PLACEHOLDER"
* widgets_placeholder?: scalar|Param|null, // Deprecated: The "widgets_placeholder" option is deprecated and will be removed in 2.0.0. No replacement. // The placeholder for the widgets. Will be replaced by the widgets management events. // Default: "//WIDGETS_PLACEHOLDER"
* clear_cache?: bool|Param, // Whether to clear the cache during the service worker activation. // Default: true
* navigation_preload?: bool|Param, // Whether to enable navigation preload. This speeds up navigation requests by making the network request in parallel with service worker boot-up. Note: Do not enable if you are precaching HTML pages (e.g., with offline_fallback or warm_cache_urls), as it would be redundant. // Default: false
* config?: array{
* debug?: bool|Param, // Controls workbox debug logging. Set to false to disable debug mode and logging. // Default: true
* version?: scalar|Param|null, // The version of workbox. When using local files, the version shall be "7.0.0." // Default: "7.3.0"
* use_cdn?: bool|Param, // Whether to use the local workbox or the CDN. // Default: false
* workbox_public_url?: scalar|Param|null, // The public path to the local workbox. Only used if use_cdn is false. // Default: "/workbox"
* },
* offline_fallback?: array{
* cache_name?: scalar|Param|null, // The name of the offline cache. // Default: "offline"
* page?: string|array{ // The URL of the offline page fallback.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* image?: string|array{ // The URL of the offline image fallback.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* font?: string|array{ // The URL of the offline font fallback.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* },
* image_cache?: bool|array{
* enabled?: bool|Param, // Default: true
* cache_name?: scalar|Param|null, // The name of the image cache. // Default: "images"
* regex?: scalar|Param|null, // The regex to match the images. // Default: "/\\.(ico|png|jpe?g|gif|svg|webp|bmp)$/"
* max_entries?: int|Param, // The maximum number of entries in the image cache. // Default: 60
* max_age?: scalar|Param|null, // The maximum number of seconds before the image cache is invalidated. // Default: 31536000
* },
* asset_cache?: bool|array{
* enabled?: bool|Param, // Default: true
* cache_name?: scalar|Param|null, // The name of the asset cache. // Default: "assets"
* regex?: scalar|Param|null, // The regex to match the assets. // Default: "/\\.(css|js|json|xml|txt|map|ico|png|jpe?g|gif|svg|webp|bmp)$/"
* max_age?: scalar|Param|null, // The maximum number of seconds before the asset cache is invalidated. // Default: 31536000
* },
* font_cache?: bool|array{
* enabled?: bool|Param, // Default: true
* cache_name?: scalar|Param|null, // The name of the font cache. // Default: "fonts"
* regex?: scalar|Param|null, // The regex to match the fonts. // Default: "/\\.(ttf|eot|otf|woff2)$/"
* max_entries?: int|Param, // The maximum number of entries in the image cache. // Default: 60
* max_age?: int|Param, // The maximum number of seconds before the font cache is invalidated. // Default: 31536000
* },
* resource_caches?: list<array{ // Default: []
* match_callback?: scalar|Param|null, // The regex or callback function to match the URLs.
* cache_name?: scalar|Param|null, // The name of the page cache.
* network_timeout?: int|Param, // The network timeout in seconds before cache is called (for "NetworkFirst" and "NetworkOnly" strategies). // Default: 3
* strategy?: scalar|Param|null, // The caching strategy. Only "NetworkFirst", "CacheFirst" and "StaleWhileRevalidate" are supported. StaleWhileRevalidate provides instant page loads with background updates. // Default: "StaleWhileRevalidate"
* max_entries?: scalar|Param|null, // The maximum number of entries in the cache (for "CacheFirst" and "NetworkFirst" strategy only). // Default: null
* max_age?: scalar|Param|null, // The maximum number of seconds before the cache is invalidated (for "CacheFirst" and "NetWorkFirst" strategy only). // Default: null
* broadcast?: bool|Param, // Whether to broadcast the cache update events (for "StaleWhileRevalidate" strategy only). Enables client notification when content is updated. // Default: true
* range_requests?: bool|Param, // Whether to support range requests (for "CacheFirst" strategy only). // Default: false
* cacheable_response_headers?: list<scalar|Param|null>,
* cacheable_response_statuses?: list<int|Param>,
* broadcast_headers?: list<scalar|Param|null>,
* preload_urls?: list<string|array{ // Default: []
* path?: scalar|Param|null, // The URL of the shortcut.
* params?: list<mixed>,
* }>,
* }>,
* background_sync?: list<array{ // Default: []
* queue_name?: scalar|Param|null, // The name of the queue.
* match_callback?: scalar|Param|null, // The regex or callback function to match the URLs.
* error_on_4xx?: bool|Param, // Whether to retry the request on 4xx errors. // Default: true
* error_on_5xx?: bool|Param, // Whether to retry the request on 5xx errors. // Default: true
* expected_status_codes?: list<int|Param>,
* expect_redirect?: bool|Param, // Whether to expect a redirect (JS response type should be "opaqueredirect" or the "redirected" property is "true"). // Default: false
* method?: scalar|Param|null, // The HTTP method. // Default: "POST"
* broadcast_channel?: scalar|Param|null, // The broadcast channel. Set null to disable. // Default: null
* max_retention_time?: int|Param, // The maximum retention time in minutes. // Default: 1440
* force_sync_fallback?: bool|Param, // If `true`, instead of attempting to use background sync events, always attempt to replay queued request at service worker startup. Most folks will not need this, unless you explicitly target a runtime like Electron that exposes the interfaces for background sync, but does not have a working implementation. // Default: false
* }>,
* background_fetch?: bool|array{
* enabled?: bool|Param, // Default: false
* db_name?: scalar|Param|null, // The IndexDB name where downloads are stored // Default: "bgfetch-completed"
* progress_url?: string|array{ // The URL of the progress page.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* success_url?: string|array{ // The URL of the success page.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* success_message?: scalar|Param|null, // The message to display on success. This message is translated. // Default: null
* failure_message?: scalar|Param|null, // The message to display on success. This message is translated. // Default: null
* },
* image_cache_name?: scalar|Param|null, // Deprecated: The "image_cache_name" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.image_cache.cache_name" instead. // The name of the image cache. // Default: "images"
* font_cache_name?: scalar|Param|null, // Deprecated: The "font_cache_name" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.font_cache.cache_name" instead. // The name of the font cache. // Default: "fonts"
* page_cache_name?: scalar|Param|null, // Deprecated: The "page_cache_name" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.resource_caches[].cache_name" instead. // The name of the page cache. // Default: "pages"
* asset_cache_name?: scalar|Param|null, // Deprecated: The "asset_cache_name" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.asset_cache.cache_name" instead. // The name of the asset cache. // Default: "assets"
* page_fallback?: string|array{ // The URL of the offline page fallback.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* image_fallback?: string|array{ // The URL of the offline image fallback.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* font_fallback?: string|array{ // The URL of the offline font fallback.
* path?: scalar|Param|null, // The URL or route name.
* path_type_reference?: int|Param, // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
* params?: list<mixed>,
* },
* image_regex?: scalar|Param|null, // Deprecated: The "image_regex" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.image_cache.regex" instead. // The regex to match the images. // Default: "/\\.(ico|png|jpe?g|gif|svg|webp|bmp)$/"
* static_regex?: scalar|Param|null, // Deprecated: The "static_regex" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.asset_cache.regex" instead. // The regex to match the static files. // Default: "/\\.(css|js|json|xml|txt|map)$/"
* font_regex?: scalar|Param|null, // Deprecated: The "font_regex" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.font_cache.regex" instead. // The regex to match the static files. // Default: "/\\.(ttf|eot|otf|woff2)$/"
* max_image_cache_entries?: int|Param, // Deprecated: The "max_image_cache_entries" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.image_cache.max_entries" instead. // The maximum number of entries in the image cache. // Default: 60
* max_image_age?: int|Param, // Deprecated: The "max_image_age" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.image_cache.max_age" instead. // The maximum number of seconds before the image cache is invalidated. // Default: 31536000
* max_font_cache_entries?: int|Param, // Deprecated: The "max_font_cache_entries" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.font_cache.max_entries" instead. // The maximum number of entries in the font cache. // Default: 30
* max_font_age?: int|Param, // Deprecated: The "max_font_age" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.font_cache.max_age" instead. // The maximum number of seconds before the font cache is invalidated. // Default: 31536000
* network_timeout_seconds?: int|Param, // Deprecated: The "network_timeout_seconds" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.resource_caches[].network_timeout" instead. // The network timeout in seconds before cache is called (for warm cache URLs only). // Default: 3
* warm_cache_urls?: list<string|array{ // Default: []
* path?: scalar|Param|null, // The URL of the shortcut.
* params?: list<mixed>,
* }>,
* },
* },
* speculation_rules?: bool|array{ // Speculation Rules API configuration for prefetching and prerendering pages.
* enabled?: bool|Param, // Default: false
* prefetch?: list<array{ // Default: []
* source?: "list"|"document"|Param, // The source type: "list" for explicit URLs, "document" for link matching. // Default: "document"
* urls?: list<string|array{ // Default: []
* path?: scalar|Param|null, // The URL path or route name.
* params?: list<mixed>,
* }>,
* selector_matches?: scalar|Param|null, // For "document" source: CSS selector to match links. // Default: null
* href_matches?: scalar|Param|null, // For "document" source: URL pattern to match href attributes. // Default: null
* eagerness?: "immediate"|"eager"|"moderate"|"conservative"|Param, // Eagerness level: "immediate" (viewport), "eager" (hover 200ms), "moderate" (hover 100ms), "conservative" (mousedown/touchstart). // Default: "moderate"
* referrer_policy?: scalar|Param|null, // Referrer policy for the speculative request. // Default: null
* }>,
* prerender?: list<array{ // Default: []
* source?: "list"|"document"|Param, // The source type: "list" for explicit URLs, "document" for link matching. // Default: "document"
* urls?: list<string|array{ // Default: []
* path?: scalar|Param|null, // The URL path or route name.
* params?: list<mixed>,
* }>,
* selector_matches?: scalar|Param|null, // For "document" source: CSS selector to match links. // Default: null
* href_matches?: scalar|Param|null, // For "document" source: URL pattern to match href attributes. // Default: null
* eagerness?: "immediate"|"eager"|"moderate"|"conservative"|Param, // Eagerness level. For prerender, "conservative" is recommended. // Default: "conservative"
* referrer_policy?: scalar|Param|null, // Referrer policy for the speculative request. // Default: null
* }>,
* },
* web_client?: scalar|Param|null, // The Panther Client for generating screenshots. If not set, the default client will be used. // Default: null
* user_agent?: scalar|Param|null, // The user agent to use when generating screenshots. When this user agent is detected, the Symfony profiler and debug toolbar will be automatically disabled to ensure screenshots look like production. // Default: "PWAScreenshotBot"
* }
* @psalm-type VichUploaderConfig = array{
* default_filename_attribute_suffix?: scalar|Param|null, // Default: "_name"
* db_driver?: scalar|Param|null,
* storage?: scalar|Param|null, // Default: "file_system"
* use_flysystem_to_resolve_uri?: bool|Param, // Default: false
* twig?: scalar|Param|null, // twig requires templating // Default: true
* form?: scalar|Param|null, // Default: true
* metadata?: array{
* cache?: scalar|Param|null, // Default: "file"
* type?: scalar|Param|null, // Default: "attribute"
* file_cache?: array{
* dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/vich_uploader"
* },
* auto_detection?: bool|Param, // Default: true
* directories?: list<array{ // Default: []
* path?: scalar|Param|null,
* namespace_prefix?: scalar|Param|null, // Default: ""
* }>,
* },
* mappings?: array<string, array{ // Default: []
* uri_prefix?: scalar|Param|null, // Default: "/uploads"
* upload_destination?: scalar|Param|null, // Default: null
* namer?: string|array{
* service?: scalar|Param|null, // Default: null
* options?: mixed, // Default: null
* },
* directory_namer?: string|array{
* service?: scalar|Param|null, // Default: null
* options?: mixed, // Default: null
* },
* delete_on_remove?: scalar|Param|null, // Default: true
* erase_fields?: scalar|Param|null, // Default: true
* delete_on_update?: scalar|Param|null, // Default: true
* inject_on_load?: scalar|Param|null, // Default: false
* namer_keep_extension?: scalar|Param|null, // Default: false
* db_driver?: scalar|Param|null, // Default: null
* }>,
* }
* @psalm-type SchebTwoFactorConfig = array{
* persister?: scalar|Param|null, // Default: "scheb_two_factor.persister.doctrine"
* model_manager_name?: scalar|Param|null, // Default: null
* security_tokens?: list<scalar|Param|null>,
* ip_whitelist?: list<scalar|Param|null>,
* ip_whitelist_provider?: scalar|Param|null, // Default: "scheb_two_factor.default_ip_whitelist_provider"
* two_factor_token_factory?: scalar|Param|null, // Default: "scheb_two_factor.default_token_factory"
* two_factor_provider_decider?: scalar|Param|null, // Default: "scheb_two_factor.default_provider_decider"
* two_factor_condition?: scalar|Param|null, // Default: null
* code_reuse_cache?: scalar|Param|null, // Default: null
* code_reuse_cache_duration?: int|Param, // Default: 60
* code_reuse_default_handler?: scalar|Param|null, // Default: null
* backup_codes?: bool|array{
* enabled?: scalar|Param|null, // Default: false
* manager?: scalar|Param|null, // Default: "scheb_two_factor.default_backup_code_manager"
* },
* email?: bool|array{
* enabled?: scalar|Param|null, // Default: false
* mailer?: scalar|Param|null, // Default: null
* code_generator?: scalar|Param|null, // Default: "scheb_two_factor.security.email.default_code_generator"
* form_renderer?: scalar|Param|null, // Default: null
* sender_email?: scalar|Param|null, // Default: null
* sender_name?: scalar|Param|null, // Default: null
* template?: scalar|Param|null, // Default: "@SchebTwoFactor/Authentication/form.html.twig"
* digits?: int|Param, // Default: 4
* },
* google?: bool|array{
* enabled?: scalar|Param|null, // Default: false
* form_renderer?: scalar|Param|null, // Default: null
* issuer?: scalar|Param|null, // Default: null
* server_name?: scalar|Param|null, // Default: null
* template?: scalar|Param|null, // Default: "@SchebTwoFactor/Authentication/form.html.twig"
* digits?: int|Param, // Default: 6
* leeway?: int|Param, // Default: 0
* },
* }
* @psalm-type ConfigType = array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
@@ -1463,11 +2283,18 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* doctrine?: DoctrineConfig,
* doctrine_migrations?: DoctrineMigrationsConfig,
* twig?: TwigConfig,
* stimulus?: StimulusConfig,
* turbo?: TurboConfig,
* twig_extra?: TwigExtraConfig,
* security?: SecurityConfig,
* monolog?: MonologConfig,
* endroid_qr_code?: EndroidQrCodeConfig,
* knp_paginator?: KnpPaginatorConfig,
* knpu_oauth2_client?: KnpuOauth2ClientConfig,
* flysystem?: FlysystemConfig,
* liip_imagine?: LiipImagineConfig,
* nelmio_security?: NelmioSecurityConfig,
* pwa?: PwaConfig,
* vich_uploader?: VichUploaderConfig,
* scheb_two_factor?: SchebTwoFactorConfig,
* "when@dev"?: array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
@@ -1478,12 +2305,19 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* debug?: DebugConfig,
* twig?: TwigConfig,
* web_profiler?: WebProfilerConfig,
* stimulus?: StimulusConfig,
* turbo?: TurboConfig,
* twig_extra?: TwigExtraConfig,
* security?: SecurityConfig,
* monolog?: MonologConfig,
* maker?: MakerConfig,
* endroid_qr_code?: EndroidQrCodeConfig,
* knp_paginator?: KnpPaginatorConfig,
* knpu_oauth2_client?: KnpuOauth2ClientConfig,
* flysystem?: FlysystemConfig,
* liip_imagine?: LiipImagineConfig,
* nelmio_security?: NelmioSecurityConfig,
* pwa?: PwaConfig,
* vich_uploader?: VichUploaderConfig,
* scheb_two_factor?: SchebTwoFactorConfig,
* },
* "when@prod"?: array{
* imports?: ImportsConfig,
@@ -1493,11 +2327,18 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* doctrine?: DoctrineConfig,
* doctrine_migrations?: DoctrineMigrationsConfig,
* twig?: TwigConfig,
* stimulus?: StimulusConfig,
* turbo?: TurboConfig,
* twig_extra?: TwigExtraConfig,
* security?: SecurityConfig,
* monolog?: MonologConfig,
* endroid_qr_code?: EndroidQrCodeConfig,
* knp_paginator?: KnpPaginatorConfig,
* knpu_oauth2_client?: KnpuOauth2ClientConfig,
* flysystem?: FlysystemConfig,
* liip_imagine?: LiipImagineConfig,
* nelmio_security?: NelmioSecurityConfig,
* pwa?: PwaConfig,
* vich_uploader?: VichUploaderConfig,
* scheb_two_factor?: SchebTwoFactorConfig,
* },
* "when@test"?: array{
* imports?: ImportsConfig,
@@ -1508,11 +2349,18 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* doctrine_migrations?: DoctrineMigrationsConfig,
* twig?: TwigConfig,
* web_profiler?: WebProfilerConfig,
* stimulus?: StimulusConfig,
* turbo?: TurboConfig,
* twig_extra?: TwigExtraConfig,
* security?: SecurityConfig,
* monolog?: MonologConfig,
* endroid_qr_code?: EndroidQrCodeConfig,
* knp_paginator?: KnpPaginatorConfig,
* knpu_oauth2_client?: KnpuOauth2ClientConfig,
* flysystem?: FlysystemConfig,
* liip_imagine?: LiipImagineConfig,
* nelmio_security?: NelmioSecurityConfig,
* pwa?: PwaConfig,
* vich_uploader?: VichUploaderConfig,
* scheb_two_factor?: SchebTwoFactorConfig,
* },
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
* imports?: ImportsConfig,

View File

@@ -0,0 +1,2 @@
_liip_imagine:
resource: "@LiipImagineBundle/Resources/config/routing.yaml"

View File

@@ -0,0 +1,7 @@
2fa_login:
path: /2fa
defaults:
_controller: "scheb_two_factor.form_controller::form"
2fa_login_check:
path: /2fa_check

View File

@@ -7,6 +7,7 @@
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
admin_email: '%env(ADMIN_EMAIL)%'
services:
# default configuration for services in *this* file
@@ -19,5 +20,6 @@ services:
App\:
resource: '../src/'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
App\Twig\ViteAssetExtension:
arguments:
$manifest: '%kernel.project_dir%/public/build/.vite/manifest.json'

193
docker-compose-dev.yml Normal file
View File

@@ -0,0 +1,193 @@
services:
php:
build:
context: ./docker/php/dev
dockerfile: Dockerfile
container_name: crm_ecosplay_php
restart: unless-stopped
group_add:
- "${DOCKER_GID:-989}"
volumes:
- .:/app
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- "9000:9000"
depends_on:
pgbouncer:
condition: service_healthy
redis:
condition: service_healthy
database:
image: postgres:16-alpine
container_name: crm_ecosplay_database
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: crm_ecosplay
ports:
- "5432:5432"
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d crm_ecosplay"]
interval: 5s
timeout: 5s
retries: 5
pgbouncer:
image: edoburu/pgbouncer
container_name: crm_ecosplay_pgbouncer
volumes:
- ./docker/pgsql/pgbouncer-dev.ini:/etc/pgbouncer/pgbouncer.ini:ro
- ./docker/pgsql/userlist-dev.txt:/etc/pgbouncer/userlist.txt:ro
ports:
- "6432:6432"
depends_on:
database:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 6432 -U app"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: crm_ecosplay_redis
command: redis-server --requirepass crm_ecosplay
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "crm_ecosplay", "ping"]
interval: 5s
timeout: 5s
retries: 5
caddy:
image: caddy:2-alpine
container_name: crm_ecosplay_caddy
ports:
- "8000:80"
volumes:
- ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile
- .:/app
depends_on:
- php
messenger:
build:
context: ./docker/php/dev
dockerfile: Dockerfile
container_name: crm_ecosplay_messenger
command: php bin/console messenger:consume async -vv
restart: unless-stopped
volumes:
- .:/app
depends_on:
pgbouncer:
condition: service_healthy
redis:
condition: service_healthy
meilisearch:
condition: service_started
bun:
image: oven/bun:alpine
container_name: crm_ecosplay_bun
working_dir: /app
volumes:
- .:/app
- bun-modules:/app/node_modules
command: sh -c "bun install && bun run dev"
ports:
- "5173:5173"
mailpit:
image: axllent/mailpit
container_name: crm_ecosplay_mailpit
ports:
- "1025:1025"
- "8025:8025"
vault:
image: hashicorp/vault:latest
container_name: crm_ecosplay_vault
cap_add:
- IPC_LOCK
environment:
VAULT_DEV_ROOT_TOKEN_ID: crm_ecosplay
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
ports:
- "8200:8200"
volumes:
- vault-data:/vault/file
ngrok:
image: ngrok/ngrok:latest
container_name: crm_ecosplay_ngrok
command: http caddy:80 --log stdout
environment:
NGROK_AUTHTOKEN: GXtZtKtRxRF5TFV5pCKD_25f1ALUyQQ9LkyQJgv1dr
ports:
- "4040:4040"
depends_on:
- caddy
ngrok-sync:
image: curlimages/curl:latest
container_name: crm_ecosplay_ngrok_sync
user: "0:0"
volumes:
- .:/app
- ./docker/ngrok/sync.sh:/sync.sh
depends_on:
- ngrok
entrypoint: sh /sync.sh
meilisearch:
image: getmeili/meilisearch:latest
container_name: crm_ecosplay_meilisearch
environment:
MEILI_MASTER_KEY: crm_ecosplay
MEILI_ENV: development
ports:
- "7700:7700"
volumes:
- meilisearch-data:/meili_data
cron:
build:
context: ./docker/cron
dockerfile: Dockerfile
container_name: crm_ecosplay_cron
restart: unless-stopped
group_add:
- "${DOCKER_GID:-989}"
volumes:
- .:/app
- /var/run/docker.sock:/var/run/docker.sock:ro
depends_on:
pgbouncer:
condition: service_healthy
redis:
condition: service_healthy
redisinsight:
image: redis/redisinsight:latest
container_name: crm_ecosplay_redisinsight
ports:
- "5540:5540"
depends_on:
redis:
condition: service_healthy
volumes:
db-data:
redis-data:
bun-modules:
vault-data:
meilisearch-data:
libretranslate-data:

34
docker/caddy/Caddyfile Normal file
View File

@@ -0,0 +1,34 @@
:80 {
root * /app/public
handle_path /stats/* {
rewrite * {uri}
reverse_proxy https://tools-security.esy-web.dev {
header_up Host tools-security.esy-web.dev
}
}
handle /assets/perf.js {
rewrite * /beacon.min.js
reverse_proxy https://static.cloudflareinsights.com {
header_up Host static.cloudflareinsights.com
}
}
handle_path /sperf {
rewrite * /cdn-cgi/rum
reverse_proxy https://cloudflareinsights.com {
header_up Host cloudflareinsights.com
}
}
php_fastcgi php:9000 {
trusted_proxies private_ranges
}
file_server
encode gzip
try_files {path} /index.php?{query}
}

25
docker/cron/Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
FROM php:8.4-fpm
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
libzip-dev \
libicu-dev \
cron \
&& rm -rf /var/lib/apt/lists/* \
&& docker-php-ext-install pdo_pgsql intl \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& groupadd -g 1000 appuser && useradd -u 1000 -g appuser -m appuser
COPY crontab /etc/cron.d/app-crontab
RUN chmod 0644 /etc/cron.d/app-crontab && crontab -u appuser /etc/cron.d/app-crontab
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
WORKDIR /app
# cron daemon requires root to manage user crontabs, jobs run as appuser
USER root
CMD ["/entrypoint.sh"]

7
docker/cron/crontab Normal file
View File

@@ -0,0 +1,7 @@
*/5 * * * * echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] START app:orders:expire-pending" >> /proc/1/fd/1 && php /app/bin/console app:orders:expire-pending --env=dev >> /proc/1/fd/1 2>&1 && echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] END app:orders:expire-pending" >> /proc/1/fd/1
0 * * * * echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] START app:monitor:messenger" >> /proc/1/fd/1 && php /app/bin/console app:monitor:messenger --env=dev >> /proc/1/fd/1 2>&1 && echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] END app:monitor:messenger" >> /proc/1/fd/1
0 3 * * * echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] START app:meilisearch:check-consistency" >> /proc/1/fd/1 && php /app/bin/console app:meilisearch:check-consistency --fix --env=dev >> /proc/1/fd/1 2>&1 && echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] END app:meilisearch:check-consistency" >> /proc/1/fd/1
0 */6 * * * echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] START app:stripe:sync" >> /proc/1/fd/1 && php /app/bin/console app:stripe:sync --env=dev >> /proc/1/fd/1 2>&1 && echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] END app:stripe:sync" >> /proc/1/fd/1
*/5 * * * * php /app/bin/console app:infra:snapshot --env=dev >> /proc/1/fd/1 2>&1
0 4 * * * echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] START app:attestations:clean" >> /proc/1/fd/1 && php /app/bin/console app:attestations:clean --env=dev >> /proc/1/fd/1 2>&1 && echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] END app:attestations:clean" >> /proc/1/fd/1
*/15 * * * * echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] START app:services:check" >> /proc/1/fd/1 && php /app/bin/console app:services:check --env=dev >> /proc/1/fd/1 2>&1 && echo "[$(date '+\%Y-\%m-\%d \%H:\%M:\%S')] END app:services:check" >> /proc/1/fd/1

10
docker/cron/entrypoint.sh Normal file
View File

@@ -0,0 +1,10 @@
#!/bin/sh
echo "=== CRM Ecosplay Cron ==="
echo "Registered tasks:"
echo " - */5 * * * * app:orders:expire-pending"
echo " - 0 * * * * app:monitor:messenger"
echo " - 0 3 * * * app:meilisearch:check-consistency --fix"
echo " - 0 4 * * * app:attestations:clean"
echo " - */15 * * * app:services:check"
echo "===================="
exec cron -f

29
docker/ngrok/sync.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/sh
set -e
echo "Waiting for ngrok to start..."
sleep 5
NGROK_URL=""
RETRIES=10
while [ -z "$NGROK_URL" ] && [ "$RETRIES" -gt 0 ]; do
NGROK_URL=$(curl -s http://ngrok:4040/api/tunnels | grep -o '"public_url":"https://[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$NGROK_URL" ]; then
echo "Waiting for tunnel..."
sleep 2
RETRIES=$((RETRIES - 1))
fi
done
if [ -z "$NGROK_URL" ]; then
echo "ERROR: Could not get ngrok URL"
exit 1
fi
touch /app/.env.local
sed -i '/^OUTSIDE_URL=/d' /app/.env.local
echo "OUTSIDE_URL=$NGROK_URL" >> /app/.env.local
echo "Ngrok URL: $NGROK_URL"
echo "Written to .env.local"

12
docker/pgsql/init-master.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'crm-ecosplay';
SELECT pg_create_physical_replication_slot('slave_slot');
EOSQL
echo "host replication replicator 0.0.0.0/0 md5" >> "$PGDATA/pg_hba.conf"
echo "host all all 0.0.0.0/0 md5" >> "$PGDATA/pg_hba.conf"
pg_ctl reload -D "$PGDATA"

View File

@@ -0,0 +1,2 @@
CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'crm-ecosplay';
SELECT pg_create_physical_replication_slot('slave_slot');

20
docker/pgsql/init-slave.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
set -e
until pg_isready -h db-master -U crm-ecosplay; do
echo "Waiting for master..."
sleep 2
done
echo "db-master:5432:replication:replicator:crm-ecosplay" > /tmp/.pgpass
chown postgres:postgres /tmp/.pgpass
chmod 600 /tmp/.pgpass
rm -rf "$PGDATA"/*
chown postgres:postgres "$PGDATA"
chmod 700 "$PGDATA"
su-exec postgres env PGPASSFILE=/tmp/.pgpass pg_basebackup -h db-master -D "$PGDATA" -U replicator -Fp -Xs -P -R
echo "hot_standby = on" >> "$PGDATA/postgresql.conf"
exec su-exec postgres "$@"

View File

@@ -0,0 +1,22 @@
[databases]
crm_ecosplay = host=database port=5432 dbname=crm_ecosplay
crm_ecosplay_test = host=database port=5432 dbname=crm_ecosplay_test
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = plain
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 200
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
server_lifetime = 3600
server_idle_timeout = 600
log_connections = 0
log_disconnections = 0
ignore_startup_parameters = extra_float_digits
admin_users = app
stats_users = app

View File

@@ -0,0 +1 @@
"app" "secret"

35
docker/php/dev/Dockerfile Normal file
View File

@@ -0,0 +1,35 @@
# hadolint global ignore=DL3008
FROM php:8.4-fpm
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
libsqlite3-dev \
libzip-dev \
libxml2-dev \
libicu-dev \
libpng-dev \
libjpeg-dev \
libfreetype-dev \
libmagickwand-dev \
unzip \
qpdf \
curl \
python3 \
python3-pip \
git \
&& rm -rf /var/lib/apt/lists/* \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install \
pdo_mysql \
pdo_pgsql \
pdo_sqlite \
zip \
intl \
gd \
&& pecl install redis imagick pcov \
&& docker-php-ext-enable redis imagick pcov \
&& groupadd -g 1000 appuser && useradd -u 1000 -g appuser -m appuser
WORKDIR /app
USER appuser

View File

@@ -0,0 +1,35 @@
# hadolint global ignore=DL3008
FROM php:8.4-fpm
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
libsqlite3-dev \
libzip-dev \
libxml2-dev \
libicu-dev \
libpng-dev \
libjpeg-dev \
libfreetype-dev \
libmagickwand-dev \
unzip \
&& rm -rf /var/lib/apt/lists/* \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install \
pdo_mysql \
pdo_pgsql \
pdo_sqlite \
zip \
intl \
gd \
exif \
opcache \
&& pecl install redis imagick \
&& docker-php-ext-enable redis imagick \
&& groupadd -g 1000 appuser && useradd -u 1000 -g appuser -m appuser
COPY php.ini /usr/local/etc/php/conf.d/app.ini
COPY opcache.ini /usr/local/etc/php/conf.d/opcache.ini
WORKDIR /app
USER appuser

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