```
✨ feat(security): Ajoute l'authentification Keycloak SSO et migre les commandes
Supprime la commande AccountCommand, la migration et ajoute l'authentification
Keycloak SSO. Crée les vues de base pour le tableau de bord.
```
This commit is contained in:
4
.env
4
.env
@@ -67,3 +67,7 @@ GOOGLE_APPLICATION_CREDENTIALS=%kernel.project_dir%/google.json
|
|||||||
SENTRY_DSN=""
|
SENTRY_DSN=""
|
||||||
###< sentry/sentry-symfony ###
|
###< sentry/sentry-symfony ###
|
||||||
DEFAULT_URI=https://esyweb.local
|
DEFAULT_URI=https://esyweb.local
|
||||||
|
KEYCLOAK_AUTH_SERVER_URL=https://auth.esy-web.dev
|
||||||
|
KEYCLOAK_REALM=master
|
||||||
|
KEYCLOAK_CLIENT_ID=ludikevent
|
||||||
|
KEYCLOAK_CLIENT_SECRET=FA7ue4h6rKL0bFZSEXxoZ4uh5LIohsyd
|
||||||
|
|||||||
34
.gitea/workflows/install-deps.yml
Normal file
34
.gitea/workflows/install-deps.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Nom du workflow
|
||||||
|
name: Symfony CI - Install, Test, Build, Attest & Deploy
|
||||||
|
|
||||||
|
# Déclencheurs du workflow
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master # Ou 'main'
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
branches:
|
||||||
|
- master # Ou 'main'
|
||||||
|
|
||||||
|
# Permissions nécessaires pour les actions utilisées
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
|
security-events: write # Requis pour Snyk pour poster les résultats
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: 🚀 Deploy to Production
|
||||||
|
steps:
|
||||||
|
- name: Deploy with SSH & Ansible
|
||||||
|
uses: appleboy/ssh-action@v1.0.0
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
port: 22
|
||||||
|
script: |
|
||||||
|
cd /var/www/ludikevent-intranet && git pull && nohup sh ./update.sh
|
||||||
2
ansible/hosts.ini
Normal file
2
ansible/hosts.ini
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[webservers]
|
||||||
|
127.0.0.1 ansible_connection=local ansible_python_interpreter=/usr/bin/python3 path=/var/www/ludikevent-intranet
|
||||||
220
ansible/playbook.yml
Normal file
220
ansible/playbook.yml
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
# Fichier: install_php_83_symfony_pgsql.yml
|
||||||
|
|
||||||
|
- name: Deploy application
|
||||||
|
hosts: webservers
|
||||||
|
become: true
|
||||||
|
gather_facts: true
|
||||||
|
|
||||||
|
vars:
|
||||||
|
db_name: "ludikevent"
|
||||||
|
db_user: "ludikevent"
|
||||||
|
db_password: "ludikevent"
|
||||||
|
redis_password: "ludikevent"
|
||||||
|
redis_port: "20110"
|
||||||
|
# Assurez-vous que 'path' est définie dans votre inventaire ou comme extra-var
|
||||||
|
# Exemple: path: /var/www/mainframe/app
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Exécuter 'composer install' dans le répertoire de l'application
|
||||||
|
ansible.builtin.command: composer install --no-dev --optimize-autoloader
|
||||||
|
become: false # Run as the connection user (e.g., 'bot')
|
||||||
|
args:
|
||||||
|
chdir: "{{ path }}"
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
- name: Send a message to the Discord channel
|
||||||
|
community.general.discord:
|
||||||
|
webhook_id: "1419573620602044518"
|
||||||
|
webhook_token: "ikAdxWxsrrTqMTb5Gh_8ylcoJHlOnq7aJZvR5udoS_fCK56Jk3qpEnJHVKdD8fwuNJF3"
|
||||||
|
content: "Mise à jour du intranet ludikevent"
|
||||||
|
|
||||||
|
- name: Installer le support ACL pour corriger les permissions de 'become_user'
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: acl
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
|
||||||
|
- name: Installation des dépendances pour le module Ansible PostgreSQL
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: python3-psycopg2
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
|
||||||
|
- name: Installation de PHP 8.3 et PHP 8.3-FPM avec les dépendances
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name:
|
||||||
|
- php8.3
|
||||||
|
- php8.3-fpm
|
||||||
|
- php8.3-cli
|
||||||
|
- php8.3-common
|
||||||
|
- php8.3-mysql
|
||||||
|
- php8.3-pgsql
|
||||||
|
- php8.3-xml
|
||||||
|
- php8.3-mbstring
|
||||||
|
- php8.3-zip
|
||||||
|
- php8.3-intl
|
||||||
|
- php8.3-gd
|
||||||
|
- php8.3-curl
|
||||||
|
- php8.3-pdo
|
||||||
|
- php8.3-opcache
|
||||||
|
- php8.3-bcmath
|
||||||
|
- php8.3-redis
|
||||||
|
- php8.3-imagick
|
||||||
|
- ffmpeg
|
||||||
|
state: present
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
|
||||||
|
- name: Démarrage et activation du service PHP 8.3 FPM
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: php8.3-fpm
|
||||||
|
state: started
|
||||||
|
enabled: yes
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
|
||||||
|
- name: Créer le fichier .env.local avec les secrets de production
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: |
|
||||||
|
APP_ENV=prod
|
||||||
|
VITE_LOAD=1
|
||||||
|
DATABASE_URL="postgresql://{{ db_user }}:{{ db_password }}@127.0.0.1:5432/{{ db_name }}?serverVersion=16&charset=utf8"
|
||||||
|
REDIS_DSN="redis://{{ redis_password }}@127.0.0.1:{{ redis_port }}"
|
||||||
|
REDIS_URL="redis://{{ redis_password }}@127.0.0.1:{{ redis_port }}"
|
||||||
|
MESSENGER_TRANSPORT_DSN="redis://{{ redis_password }}@127.0.0.1:{{ redis_port }}/messages"
|
||||||
|
APP_SECRET=939bbc67038c2e2d1232d86fc605bf2f
|
||||||
|
REAL_MAIL=1
|
||||||
|
VAULT_ADDR=http://127.0.0.1:8200
|
||||||
|
VAULT_TOKEN=hvs.QLpUdiptXtSPo5Qf7i2nn2Xz
|
||||||
|
MAILER_DSN=ses+smtp://AKIAWTT2T22CWBRBBDYN:BBdgb6KxRQ8mNcpWFJsZCJxbSGNdgLhKFiITMErfBlQP@default?region=eu-west-3
|
||||||
|
|
||||||
|
dest: "{{ path }}/.env.local"
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
|
||||||
|
# --- Initial creation of essential directories with correct ownership ---
|
||||||
|
# These directories should exist before composer runs, but composer might create subdirs.
|
||||||
|
- name: Ensure app/var and public/media directories exist with correct owner/group
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
owner: bot # Assuming 'bot' is your deployment user
|
||||||
|
group: www-data
|
||||||
|
mode: '0775' # Allow 'bot' and 'www-data' to read/write/execute
|
||||||
|
state: directory
|
||||||
|
recurse: yes # Important to ensure subdirectories created by previous deploys also get permissions
|
||||||
|
loop:
|
||||||
|
- "{{ path }}/var"
|
||||||
|
- "{{ path }}/var/log" # Specific for log, though var/log might be created by composer later
|
||||||
|
- "{{ path }}/public/media" # For uploads
|
||||||
|
- "{{ path }}/public/storage" # For uploads
|
||||||
|
- "{{ path }}/public/tmp-sign" # For uploads
|
||||||
|
|
||||||
|
# --- POST-COMPOSER PERMISSION FIXES ---
|
||||||
|
# This is crucial because composer creates var/cache as the `become: false` user
|
||||||
|
- name: Set correct permissions for Symfony cache and logs directories
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
owner: bot
|
||||||
|
group: www-data
|
||||||
|
mode: '0775' # rwx for owner and group, rx for others
|
||||||
|
state: directory
|
||||||
|
recurse: yes # Apply to all contents
|
||||||
|
loop:
|
||||||
|
- "{{ path }}/var/cache"
|
||||||
|
- "{{ path }}/var/log"
|
||||||
|
# For web-writable directories created by the app itself (e.g., uploads), you might set ACLs
|
||||||
|
# or chown to www-data and then your user gets access via group membership.
|
||||||
|
|
||||||
|
# Alternative for cache/log permissions using ACLs (more robust for mixed ownership)
|
||||||
|
# This requires 'acl' package installed (which you already do).
|
||||||
|
# Use this if 'bot' needs to own, but www-data needs to write.
|
||||||
|
- name: Set ACLs for Symfony cache and logs (recommended for web-writable dirs)
|
||||||
|
ansible.builtin.acl:
|
||||||
|
path: "{{ item }}"
|
||||||
|
entity: www-data
|
||||||
|
etype: group
|
||||||
|
permissions: rwx
|
||||||
|
state: present
|
||||||
|
recursive: yes
|
||||||
|
default: yes # Apply default ACLs for new files/dirs within
|
||||||
|
loop:
|
||||||
|
- "{{ path }}/var/cache"
|
||||||
|
- "{{ path }}/var/log"
|
||||||
|
when: ansible_os_family == "Debian" # ACLs are Linux-specific
|
||||||
|
|
||||||
|
- name: Exécuter bun install dans le répertoire de l application
|
||||||
|
ansible.builtin.command: bun install
|
||||||
|
become: false
|
||||||
|
args:
|
||||||
|
chdir: "{{ path }}"
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
|
||||||
|
- name: Exécuter bun build dans le répertoire de l application
|
||||||
|
ansible.builtin.command: bun run build
|
||||||
|
become: false
|
||||||
|
args:
|
||||||
|
chdir: "{{ path }}"
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
|
||||||
|
- name: Supervisor config
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: supervisor.j2
|
||||||
|
dest: "/etc/supervisor/conf.d/mainframe.conf"
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Reread Supervisor configuration
|
||||||
|
ansible.builtin.command: supervisorctl reread
|
||||||
|
changed_when: true # Always mark as changed, as output is not always useful for idempotency
|
||||||
|
|
||||||
|
- name: Update Supervisor (add/remove updated programs)
|
||||||
|
ansible.builtin.command: supervisorctl update
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Purger la base de données Redis
|
||||||
|
ansible.builtin.command: "redis-cli -p {{ redis_port }} -a {{ redis_password }} FLUSHALL"
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
|
||||||
|
- name: Generate Caddy site configuration
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: caddy.j2
|
||||||
|
dest: "/etc/caddy/sites/mainframe.conf"
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Reload Caddy to apply new configuration
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: caddy
|
||||||
|
state: reloaded
|
||||||
|
enabled: yes
|
||||||
|
- name: Exécuter doctrine:migration:migrate dans le répertoire de l application
|
||||||
|
ansible.builtin.command: php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
become: false
|
||||||
|
args:
|
||||||
|
chdir: "{{ path }}"
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
- name: Exécuter cache:clear dans le répertoire de l application
|
||||||
|
ansible.builtin.command: php bin/console cache:clear
|
||||||
|
become: false
|
||||||
|
args:
|
||||||
|
chdir: "{{ path }}"
|
||||||
|
when: ansible_os_family == "Debian"
|
||||||
|
|
||||||
|
- name: Exécuter liip:imagine:cache:remove dans le répertoire de l application
|
||||||
|
ansible.builtin.command: php bin/console liip:imagine:cache:remove
|
||||||
|
become: false
|
||||||
|
args:
|
||||||
|
chdir: "{{ path }}"
|
||||||
|
when: ansible_os_family == "Debian" # Added a when condition here, often missed
|
||||||
|
|
||||||
|
- name: Set correct permissions for Symfony cache and logs directories
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
owner: bot
|
||||||
|
group: www-data
|
||||||
|
mode: '0777' # rwx for owner and group, rx for others
|
||||||
|
state: directory
|
||||||
|
recurse: yes # Apply to all contents
|
||||||
|
loop:
|
||||||
|
- "{{ path }}/var/cache"
|
||||||
|
- "{{ path }}/var/log"
|
||||||
|
- "{{ path }}/public/media"
|
||||||
|
- "{{ path }}/public/storage" # For uploads
|
||||||
|
- "{{ path }}/public/tmp-sign" # For uploads
|
||||||
|
|
||||||
21
ansible/templates/caddy.j2
Normal file
21
ansible/templates/caddy.j2
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
intranet.ludikevent.fr{
|
||||||
|
tls {
|
||||||
|
dns cloudflare KL6pZ-Z_12_zbnM2TtFDIsKM8A-HLPhU5GJJbKTW
|
||||||
|
}
|
||||||
|
root * {{ path }}/public
|
||||||
|
|
||||||
|
file_server
|
||||||
|
request_body {
|
||||||
|
max_size 100MB
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
Permissions-Policy "accelerometer=(), autoplay=(), camera=(), clipboard-write=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), usb=(), vr=(), screen-wake-lock=(), xr-spatial-tracking=(), bluetooth=(), ambient-light-sensor=(), battery=(), gamepad=(), notifications=(), push=()"
|
||||||
|
}
|
||||||
|
|
||||||
|
php_fastcgi unix//run/php/php8.3-fpm.sock {
|
||||||
|
read_timeout 300s
|
||||||
|
write_timeout 300s
|
||||||
|
dial_timeout 100s
|
||||||
|
env HTTP_PROXY ""
|
||||||
|
}
|
||||||
|
}
|
||||||
17
ansible/templates/supervisor.j2
Normal file
17
ansible/templates/supervisor.j2
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[program:redis_ludikevent_intranet]
|
||||||
|
command=redis-server --port {{ redis_port }} --requirepass {{ redis_password }}
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
user=root
|
||||||
|
stdout_logfile=/var/www/ludikevent-intranet/var/log/redis_stdout.log
|
||||||
|
stderr_logfile=/var/www/ludikevent-intranet/var/log/redis_stderr.log
|
||||||
|
|
||||||
|
[program:messenger_redis_ludikevent_intranet]
|
||||||
|
command=php {{path}}/bin/console messenger:consume async --time-limit=3600
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
user=root
|
||||||
|
startsecs=0
|
||||||
|
startretries=10
|
||||||
|
stdout_logfile=/var/www/ludikevent-intranet/var/log/messenger_stderr.log
|
||||||
|
stderr_logfile=/var/www/ludikevent-intranet/var/log/messenger_stdout.log
|
||||||
@@ -11,40 +11,42 @@
|
|||||||
"ext-libxml": "*",
|
"ext-libxml": "*",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"chillerlan/php-qrcode": ">=5.0.5",
|
"chillerlan/php-qrcode": ">=5.0.5",
|
||||||
"cocur/slugify": ">=4.6",
|
"cocur/slugify": ">=4.7.1",
|
||||||
"doctrine/dbal": "^3.10.3",
|
"doctrine/dbal": "^3.10.4",
|
||||||
"doctrine/doctrine-bundle": "^2.18.1",
|
"doctrine/doctrine-bundle": "^2.18.2",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.7.0",
|
"doctrine/doctrine-migrations-bundle": "^3.7.0",
|
||||||
"doctrine/orm": "^3.5.7",
|
"doctrine/orm": "^3.6.1",
|
||||||
"docusealco/docuseal-php": "^1.0.5",
|
"docusealco/docuseal-php": "^1.0.5",
|
||||||
"endroid/qr-code": ">=6.0.9",
|
"endroid/qr-code": ">=6.0.9",
|
||||||
"exbil/mailcow-php-api": ">=0.15.0",
|
"exbil/mailcow-php-api": ">=0.15.0",
|
||||||
"fpdf/fpdf": ">=1.86",
|
"fpdf/fpdf": ">=1.86.1",
|
||||||
"google/apiclient": "^2.18.4",
|
"google/apiclient": "^2.19.0",
|
||||||
"google/cloud": "^0.296.0",
|
"google/cloud": "^0.296.0",
|
||||||
"healey/robots": "^1.0.1",
|
"healey/robots": "^1.0.1",
|
||||||
"imagine/imagine": "^1.5",
|
"imagine/imagine": "^1.5.2",
|
||||||
"io-developer/php-whois": ">=4.1.10",
|
"io-developer/php-whois": ">=4.1.10",
|
||||||
"knplabs/knp-paginator-bundle": "^6.9.1",
|
"knplabs/knp-paginator-bundle": "^6.10.0",
|
||||||
|
"knpuniversity/oauth2-client-bundle": "^2.20",
|
||||||
"lasserafn/php-initial-avatar-generator": "^4.5",
|
"lasserafn/php-initial-avatar-generator": "^4.5",
|
||||||
"league/flysystem-aws-s3-v3": "^3.30.1",
|
"league/flysystem-aws-s3-v3": "^3.30.1",
|
||||||
"league/flysystem-bundle": "^3.6",
|
"league/flysystem-bundle": "^3.6.1",
|
||||||
"liip/imagine-bundle": "^2.15",
|
"liip/imagine-bundle": "^2.15",
|
||||||
"lufiipe/insee-sierene": ">=1",
|
"lufiipe/insee-sierene": ">=1",
|
||||||
"minishlink/web-push": "^9.0.3",
|
"minishlink/web-push": "^9.0.4",
|
||||||
"mittwald/vault-php": "^3.0.2",
|
"mittwald/vault-php": "^3.0.2",
|
||||||
"mobiledetect/mobiledetectlib": "^4.8.09",
|
"mobiledetect/mobiledetectlib": "^4.8.10",
|
||||||
"nelmio/cors-bundle": "^2.6",
|
"nelmio/cors-bundle": "^2.6.1",
|
||||||
"ovh/ovh": ">=3.5",
|
"ovh/ovh": ">=3.5",
|
||||||
"pear/net_dns2": ">=2.0.7",
|
"pear/net_dns2": ">=2.0.7",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6.4",
|
"phpdocumentor/reflection-docblock": "^5.6.6",
|
||||||
"phpoffice/phpspreadsheet": ">=5.3",
|
"phpoffice/phpspreadsheet": ">=5.4",
|
||||||
"phpstan/phpdoc-parser": "^2.3",
|
"phpstan/phpdoc-parser": "^2.3.1",
|
||||||
"presta/sitemap-bundle": "^4.2",
|
"presta/sitemap-bundle": "^4.2",
|
||||||
"sentry/sentry-symfony": "^5.6",
|
"sentry/sentry-symfony": "^5.8.3",
|
||||||
"setasign/fpdi": "^2.6.4",
|
"setasign/fpdi": "^2.6.4",
|
||||||
"spatie/mjml-php": "^1.2.5",
|
"spatie/mjml-php": "^1.2.5",
|
||||||
"stancer/stancer": ">=2.0.1",
|
"stancer/stancer": ">=2.0.1",
|
||||||
|
"stevenmaguire/oauth2-keycloak": "^5.1",
|
||||||
"symfony/amazon-mailer": "7.3.*",
|
"symfony/amazon-mailer": "7.3.*",
|
||||||
"symfony/asset": "7.3.*",
|
"symfony/asset": "7.3.*",
|
||||||
"symfony/asset-mapper": "7.3.*",
|
"symfony/asset-mapper": "7.3.*",
|
||||||
@@ -59,7 +61,7 @@
|
|||||||
"symfony/intl": "7.3.*",
|
"symfony/intl": "7.3.*",
|
||||||
"symfony/mailer": "7.3.*",
|
"symfony/mailer": "7.3.*",
|
||||||
"symfony/mime": "7.3.*",
|
"symfony/mime": "7.3.*",
|
||||||
"symfony/monolog-bundle": "^3.10",
|
"symfony/monolog-bundle": "^3.11.1",
|
||||||
"symfony/notifier": "7.3.*",
|
"symfony/notifier": "7.3.*",
|
||||||
"symfony/process": "7.3.*",
|
"symfony/process": "7.3.*",
|
||||||
"symfony/property-access": "7.3.*",
|
"symfony/property-access": "7.3.*",
|
||||||
@@ -76,11 +78,11 @@
|
|||||||
"symfony/web-link": "7.3.*",
|
"symfony/web-link": "7.3.*",
|
||||||
"symfony/yaml": "7.3.*",
|
"symfony/yaml": "7.3.*",
|
||||||
"tecnickcom/tcpdf": "^6.10.1",
|
"tecnickcom/tcpdf": "^6.10.1",
|
||||||
"twig/extra-bundle": "^3.22.1",
|
"twig/extra-bundle": "^3.22.2",
|
||||||
"twig/intl-extra": "^3.22.1",
|
"twig/intl-extra": "^3.22.1",
|
||||||
"twig/twig": "^3.22",
|
"twig/twig": "^3.22.2",
|
||||||
"vich/uploader-bundle": "^2.8.1",
|
"vich/uploader-bundle": "^2.9.1",
|
||||||
"web-auth/webauthn-lib": ">=5.2.2"
|
"web-auth/webauthn-lib": ">=5.2.3"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
@@ -135,12 +137,12 @@
|
|||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.24.1",
|
"fakerphp/faker": "^1.24.1",
|
||||||
"phpunit/phpunit": "^12.4.4",
|
"phpunit/phpunit": "^12.5.5",
|
||||||
"rector/rector": "^2.2.8",
|
"rector/rector": "^2.3.1",
|
||||||
"symfony/browser-kit": "7.3.*",
|
"symfony/browser-kit": "7.3.*",
|
||||||
"symfony/css-selector": "7.3.*",
|
"symfony/css-selector": "7.3.*",
|
||||||
"symfony/debug-bundle": "7.3.*",
|
"symfony/debug-bundle": "7.3.*",
|
||||||
"symfony/maker-bundle": "^1.65",
|
"symfony/maker-bundle": "^1.65.1",
|
||||||
"symfony/stopwatch": "7.3.*",
|
"symfony/stopwatch": "7.3.*",
|
||||||
"symfony/web-profiler-bundle": "7.3.*"
|
"symfony/web-profiler-bundle": "7.3.*"
|
||||||
}
|
}
|
||||||
|
|||||||
967
composer.lock
generated
967
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,4 +18,5 @@ return [
|
|||||||
Presta\SitemapBundle\PrestaSitemapBundle::class => ['all' => true],
|
Presta\SitemapBundle\PrestaSitemapBundle::class => ['all' => true],
|
||||||
Sentry\SentryBundle\SentryBundle::class => ['prod' => true],
|
Sentry\SentryBundle\SentryBundle::class => ['prod' => true],
|
||||||
Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true],
|
Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true],
|
||||||
|
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
13
config/packages/knpu_oauth2_client.yaml
Normal file
13
config/packages/knpu_oauth2_client.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
knpu_oauth2_client:
|
||||||
|
clients:
|
||||||
|
# This key 'keycloak' is what you'll use in your code
|
||||||
|
keycloak:
|
||||||
|
type: keycloak
|
||||||
|
# All these should be stored in your .env file
|
||||||
|
auth_server_url: '%env(KEYCLOAK_AUTH_SERVER_URL)%'
|
||||||
|
realm: '%env(KEYCLOAK_REALM)%'
|
||||||
|
client_id: '%env(KEYCLOAK_CLIENT_ID)%'
|
||||||
|
client_secret: '%env(KEYCLOAK_CLIENT_SECRET)%'
|
||||||
|
# The route name where Keycloak will redirect the user back to
|
||||||
|
redirect_route: connect_keycloak_check
|
||||||
|
redirect_params: {}
|
||||||
@@ -27,6 +27,7 @@ security:
|
|||||||
entry_point: App\Security\AuthenticationEntryPoint
|
entry_point: App\Security\AuthenticationEntryPoint
|
||||||
custom_authenticator:
|
custom_authenticator:
|
||||||
- App\Security\LoginFormAuthenticator
|
- App\Security\LoginFormAuthenticator
|
||||||
|
- App\Security\KeycloakAuthenticator
|
||||||
logout:
|
logout:
|
||||||
target: app_logout
|
target: app_logout
|
||||||
|
|
||||||
@@ -40,9 +41,8 @@ security:
|
|||||||
# algorithm: bcrypt
|
# algorithm: bcrypt
|
||||||
|
|
||||||
role_hierarchy:
|
role_hierarchy:
|
||||||
ROLE_ROOT: [ROLE_ADMIN] # ROLE_ROOT inclut ROLE_ADMIN, qui à son tour inclut ROLE_ARTEMIS
|
ROLE_ROOT: [ROLE_ADMIN] #
|
||||||
|
|
||||||
access_control:
|
access_control:
|
||||||
- { path: ^/admin, roles: [ROLE_ADMIN] }
|
- { path: ^/admin, roles: [ROLE_ADMIN] }
|
||||||
- { path: ^/console, roles: [ROLE_COSPLAY] }
|
|
||||||
- { path: ^/, roles: PUBLIC_ACCESS } # Toutes les autres pages nécessitent une authentification complète
|
- { path: ^/, roles: PUBLIC_ACCESS } # Toutes les autres pages nécessitent une authentification complète
|
||||||
|
|||||||
@@ -111,65 +111,16 @@ services:
|
|||||||
# --- Service de Test d'Emails (MailHog) ---
|
# --- Service de Test d'Emails (MailHog) ---
|
||||||
# Intercepte tous les emails envoyés en développement
|
# Intercepte tous les emails envoyés en développement
|
||||||
mailhog:
|
mailhog:
|
||||||
image: mailhog/mailhog:latest
|
image: axllent/mailpit:latest
|
||||||
container_name: crm_mailhog
|
|
||||||
ports:
|
ports:
|
||||||
# Port 1025 pour le serveur SMTP factice
|
|
||||||
- "1025:1025"
|
- "1025:1025"
|
||||||
# Port 8025 pour l'interface web de MailHog
|
|
||||||
- "8025:8025"
|
- "8025:8025"
|
||||||
networks:
|
networks:
|
||||||
- crm_network # Assignation au réseau commun
|
- crm_network # Assignation au réseau commun
|
||||||
|
|
||||||
# --- Service de Stockage Fichiers (MinIO) ---
|
|
||||||
# Fournit une API compatible S3 pour le stockage de fichiers
|
|
||||||
minio:
|
|
||||||
image: minio/minio:RELEASE.2025-02-03T21-03-04Z
|
|
||||||
container_name: crm_minio
|
|
||||||
ports:
|
|
||||||
# Port 9000 pour l'API S3
|
|
||||||
- "9000:9000"
|
|
||||||
# Port 9001 pour la console web de MinIO
|
|
||||||
- "9001:9001"
|
|
||||||
environment:
|
|
||||||
MINIO_ROOT_USER: minio_user
|
|
||||||
MINIO_ROOT_PASSWORD: ChangeMeInProd!
|
|
||||||
volumes:
|
|
||||||
# Volume nommé pour la persistance des fichiers
|
|
||||||
- minio_data:/data
|
|
||||||
# Commande pour démarrer MinIO et lancer la console sur le bon port
|
|
||||||
command: server /data --console-address ":9001"
|
|
||||||
networks:
|
|
||||||
- crm_network # Assignation au réseau commun
|
|
||||||
|
|
||||||
# --- Service de Gestion des Secrets (HashiCorp Vault) ---
|
|
||||||
vault:
|
|
||||||
image: hashicorp/vault:latest
|
|
||||||
container_name: crm_vault
|
|
||||||
ports:
|
|
||||||
- "8210:8200" # Mappe le port 8210 de l'hôte au port 8200 du conteneur Vault
|
|
||||||
- "8211:8201" # Mappe le port 8210 de l'hôte au port 8200 du conteneur Vault
|
|
||||||
- "8212:8202" # Mappe le port 8212 de l'hôte au port 8200 du conteneur Vault
|
|
||||||
volumes:
|
|
||||||
# Volume pour la persistance des données
|
|
||||||
- vault_data:/vault
|
|
||||||
# Volume pour monter notre fichier de configuration
|
|
||||||
environment:
|
|
||||||
VAULT_DEV_ROOT_TOKEN_ID: myroot
|
|
||||||
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8201
|
|
||||||
VAULT_LOCAL_CONFIG: '{"storage": {"file": {"path": "/vault/file"}}, "listener": [{"tcp": { "address": "0.0.0.0:8200", "tls_disable": true}}], "default_lease_ttl": "168h", "max_lease_ttl": "720h", "ui": true,"disable_mlock": false}'
|
|
||||||
# Lance Vault en mode serveur avec notre fichier de configuration
|
|
||||||
cap_add:
|
|
||||||
- IPC_LOCK
|
|
||||||
command: "server -dev"
|
|
||||||
networks:
|
|
||||||
- crm_network # Assignation au réseau commun
|
|
||||||
|
|
||||||
# Définition des volumes pour la persistance des données
|
# Définition des volumes pour la persistance des données
|
||||||
volumes:
|
volumes:
|
||||||
db_data: # Pour la base de données principale de Symfony
|
db_data: # Pour la base de données principale de Symfony
|
||||||
minio_data: # Pour le stockage de fichiers MinIO
|
|
||||||
vault_data: # Pour les données de HashiCorp Vault
|
|
||||||
|
|
||||||
# Définition des réseaux
|
# Définition des réseaux
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
3
makefile
3
makefile
@@ -48,7 +48,8 @@ migrate: ## Applique les migrations
|
|||||||
composer-install: ## Installe les dépendances Composer
|
composer-install: ## Installe les dépendances Composer
|
||||||
@$(PHP_EXEC) composer install
|
@$(PHP_EXEC) composer install
|
||||||
deps: composer-install ## Alias pour composer-install
|
deps: composer-install ## Alias pour composer-install
|
||||||
|
db_remove: ## Crée la base de données
|
||||||
|
@$(CONSOLE) doctrine:database:drop --force
|
||||||
dbtest_add: ## Crée la base de données
|
dbtest_add: ## Crée la base de données
|
||||||
@$(CONSOLE) doctrine:database:create --env=test
|
@$(CONSOLE) doctrine:database:create --env=test
|
||||||
dbtest_migrate: ## Crée la base de données
|
dbtest_migrate: ## Crée la base de données
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use Doctrine\Migrations\AbstractMigration;
|
|||||||
/**
|
/**
|
||||||
* Auto-generated Migration: Please modify to your needs!
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
*/
|
*/
|
||||||
final class Version20251209163956 extends AbstractMigration
|
final class Version20251211203538 extends AbstractMigration
|
||||||
{
|
{
|
||||||
public function getDescription(): string
|
public function getDescription(): string
|
||||||
{
|
{
|
||||||
44
migrations/Version20260115165200.php
Normal file
44
migrations/Version20260115165200.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260115165200 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE account ADD keycloak_id VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE account ADD first_name VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE account ADD name VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('DROP INDEX idx_75ea56e016ba31db');
|
||||||
|
$this->addSql('DROP INDEX idx_75ea56e0e3bd61ce');
|
||||||
|
$this->addSql('DROP INDEX idx_75ea56e0fb7336f0');
|
||||||
|
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE "account" DROP keycloak_id');
|
||||||
|
$this->addSql('ALTER TABLE "account" DROP first_name');
|
||||||
|
$this->addSql('ALTER TABLE "account" DROP name');
|
||||||
|
$this->addSql('DROP INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750');
|
||||||
|
$this->addSql('CREATE INDEX idx_75ea56e016ba31db ON messenger_messages (delivered_at)');
|
||||||
|
$this->addSql('CREATE INDEX idx_75ea56e0e3bd61ce ON messenger_messages (available_at)');
|
||||||
|
$this->addSql('CREATE INDEX idx_75ea56e0fb7336f0 ON messenger_messages (queue_name)');
|
||||||
|
}
|
||||||
|
}
|
||||||
32
migrations/Version20260115165808.php
Normal file
32
migrations/Version20260115165808.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260115165808 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE account ALTER password DROP NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE "account" ALTER password SET NOT NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/assets/images/logo.png
Normal file
BIN
public/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -1,66 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Command;
|
|
||||||
|
|
||||||
use App\Entity\Account;
|
|
||||||
use App\Service\Generator\TempPasswordGenerator;
|
|
||||||
use App\Service\Mailer\Event\CreatedAdminEvent;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
use Symfony\Component\Console\Question\Question;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
|
||||||
use Symfony\Component\Uid\Uuid;
|
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
|
||||||
|
|
||||||
#[AsCommand(name: 'crm:admin')]
|
|
||||||
class AccountCommand extends Command
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly EventDispatcherInterface $eventDispatcher,
|
|
||||||
private readonly UserPasswordHasherInterface $userPasswordHasher,
|
|
||||||
private readonly EntityManagerInterface $entityManager,
|
|
||||||
?string $name = null
|
|
||||||
) {
|
|
||||||
parent::__construct($name);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
$io->title("Création d'un utilisateur administrateur");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$existingUser = $this->entityManager->getRepository(Account::class)->findOneBy(['email' => "jovann@siteconseil.fr"]);
|
|
||||||
if (!$existingUser instanceof Account) {
|
|
||||||
$password = TempPasswordGenerator::generate();
|
|
||||||
$newUser = new Account();
|
|
||||||
$newUser->setRoles(['ROLE_ROOT']);
|
|
||||||
$newUser->setUuid(Uuid::v4());
|
|
||||||
$newUser->setIsActif(true);
|
|
||||||
$newUser->setIsFirstLogin(true);
|
|
||||||
$newUser->setEmail("jovann@siteconseil.fr");
|
|
||||||
$newUser->setUsername("Jovann");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$hashedPassword = $this->userPasswordHasher->hashPassword($newUser, $password);
|
|
||||||
$newUser->setPassword($hashedPassword);
|
|
||||||
$this->eventDispatcher->dispatch(new CreatedAdminEvent($newUser,$password));
|
|
||||||
|
|
||||||
$this->entityManager->persist($newUser);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
|
|
||||||
$io->success("Utilisateur administrateur créé avec succès.");
|
|
||||||
} else {
|
|
||||||
$io->warning("Un utilisateur avec l'email existe déjà.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
40
src/Controller/Dashboard/HomeController.php
Normal file
40
src/Controller/Dashboard/HomeController.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Dashboard;
|
||||||
|
|
||||||
|
use App\Controller\EntityManagerInterface;
|
||||||
|
use App\Entity\Account;
|
||||||
|
use App\Entity\AccountResetPasswordRequest;
|
||||||
|
use App\Form\RequestPasswordConfirmType;
|
||||||
|
use App\Form\RequestPasswordRequestType;
|
||||||
|
use App\Repository\AccountRepository;
|
||||||
|
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
|
||||||
|
use App\Service\ResetPassword\Event\ResetPasswordEvent;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
|
||||||
|
class HomeController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route(path: '/crm', name: 'app_crm', options: ['sitemap' => false], methods: ['GET','POST'])]
|
||||||
|
public function crm(): Response
|
||||||
|
{
|
||||||
|
return $this->render('dashboard/home.twig');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[Route(path: '/crm/administrateur', name: 'app_crm_administrateur', options: ['sitemap' => false], methods: ['GET','POST'])]
|
||||||
|
public function administrateur(AccountRepository $accountRepository): Response
|
||||||
|
{
|
||||||
|
return $this->render('dashboard/administrateur.twig',[
|
||||||
|
'admins' => $accountRepository->findAll(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ use App\Form\RequestPasswordConfirmType;
|
|||||||
use App\Form\RequestPasswordRequestType;
|
use App\Form\RequestPasswordRequestType;
|
||||||
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
|
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
|
||||||
use App\Service\ResetPassword\Event\ResetPasswordEvent;
|
use App\Service\ResetPassword\Event\ResetPasswordEvent;
|
||||||
|
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
@@ -21,9 +22,27 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|||||||
|
|
||||||
class HomeController extends AbstractController
|
class HomeController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route(path: '/', name: 'app_home', options: ['sitemap' => false], methods: ['GET'])]
|
|
||||||
|
#[Route('/connect/keycloak', name: 'connect_keycloak_start')]
|
||||||
|
public function connect(ClientRegistry $clientRegistry)
|
||||||
|
{
|
||||||
|
// Redirects to Keycloak
|
||||||
|
return $clientRegistry
|
||||||
|
->getClient('keycloak')
|
||||||
|
->redirect(['email', 'profile','openid'], []);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/oauth/sso', name: 'connect_keycloak_check')]
|
||||||
|
public function connectCheck(Request $request)
|
||||||
|
{
|
||||||
|
// This method stays empty; the authenticator will intercept it!
|
||||||
|
}
|
||||||
|
#[Route(path: '/', name: 'app_home', options: ['sitemap' => false], methods: ['GET','POST'])]
|
||||||
public function index(AuthenticationUtils $authenticationUtils): Response
|
public function index(AuthenticationUtils $authenticationUtils): Response
|
||||||
{
|
{
|
||||||
|
if($this->getUser()){
|
||||||
|
return $this->redirectToRoute('app_crm');
|
||||||
|
}
|
||||||
return $this->render('home.twig',[
|
return $this->render('home.twig',[
|
||||||
'last_username' => $authenticationUtils->getLastUsername(),
|
'last_username' => $authenticationUtils->getLastUsername(),
|
||||||
'error' => $authenticationUtils->getLastAuthenticationError(),
|
'error' => $authenticationUtils->getLastAuthenticationError(),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class Account implements UserInterface, PasswordAuthenticatedUserInterface, \Ser
|
|||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private array $roles = [];
|
private array $roles = [];
|
||||||
|
|
||||||
#[ORM\Column]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?string $password = null;
|
private ?string $password = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(length: 255)]
|
||||||
@@ -57,6 +57,15 @@ class Account implements UserInterface, PasswordAuthenticatedUserInterface, \Ser
|
|||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?bool $isActif = null;
|
private ?bool $isActif = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $keycloakId = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $firstName = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $name = null;
|
||||||
|
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -190,7 +199,6 @@ class Account implements UserInterface, PasswordAuthenticatedUserInterface, \Ser
|
|||||||
$this->id,
|
$this->id,
|
||||||
$this->email,
|
$this->email,
|
||||||
$this->username,
|
$this->username,
|
||||||
$this->avatarFileName,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +208,6 @@ class Account implements UserInterface, PasswordAuthenticatedUserInterface, \Ser
|
|||||||
$this->id,
|
$this->id,
|
||||||
$this->email,
|
$this->email,
|
||||||
$this->username,
|
$this->username,
|
||||||
$this->avatarFileName,
|
|
||||||
) = unserialize($data);
|
) = unserialize($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,4 +252,40 @@ class Account implements UserInterface, PasswordAuthenticatedUserInterface, \Ser
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getKeycloakId(): ?string
|
||||||
|
{
|
||||||
|
return $this->keycloakId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setKeycloakId(?string $keycloakId): static
|
||||||
|
{
|
||||||
|
$this->keycloakId = $keycloakId;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFirstName(): ?string
|
||||||
|
{
|
||||||
|
return $this->firstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFirstName(?string $firstName): static
|
||||||
|
{
|
||||||
|
$this->firstName = $firstName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): ?string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(?string $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
101
src/Security/KeycloakAuthenticator.php
Normal file
101
src/Security/KeycloakAuthenticator.php
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
// src/Security/KeycloakAuthenticator.php
|
||||||
|
namespace App\Security;
|
||||||
|
|
||||||
|
use App\Entity\Account;
|
||||||
|
use App\Entity\User; // Your User entity
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
|
||||||
|
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\RouterInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||||
|
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
||||||
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
|
class KeycloakAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface
|
||||||
|
{
|
||||||
|
private $clientRegistry;
|
||||||
|
private $entityManager;
|
||||||
|
private $router;
|
||||||
|
|
||||||
|
public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $entityManager, RouterInterface $router)
|
||||||
|
{
|
||||||
|
$this->clientRegistry = $clientRegistry;
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
$this->router = $router;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(Request $request): ?bool
|
||||||
|
{
|
||||||
|
// match the route name from the controller
|
||||||
|
return $request->attributes->get('_route') === 'connect_keycloak_check';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authenticate(Request $request): Passport
|
||||||
|
{
|
||||||
|
$client = $this->clientRegistry->getClient('keycloak');
|
||||||
|
$accessToken = $this->fetchAccessToken($client);
|
||||||
|
|
||||||
|
return new SelfValidatingPassport(
|
||||||
|
new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) {
|
||||||
|
/** @var \Stevenmaguire\OAuth2\Client\Provider\KeycloakResourceOwner $keycloakUser */
|
||||||
|
$keycloakUser = $client->fetchUserFromToken($accessToken);
|
||||||
|
|
||||||
|
|
||||||
|
$email = $keycloakUser->getEmail();
|
||||||
|
|
||||||
|
$existingUser = $this->entityManager->getRepository(Account::class)->findOneBy(['keycloakId' => $keycloakUser->getId()]);
|
||||||
|
|
||||||
|
if ($existingUser) {
|
||||||
|
return $existingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Optional: Find by email if ID doesn't match (syncing)
|
||||||
|
$user = $this->entityManager->getRepository(Account::class)->findOneBy(['email' => $email]);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
// 3) Create a new user if they don't exist
|
||||||
|
$user = new Account();
|
||||||
|
$user->setUuid(Uuid::v4());
|
||||||
|
$user->setRoles(['ROLE_ROOT']);
|
||||||
|
$user->setIsActif(true);
|
||||||
|
$user->setIsFirstLogin(false);
|
||||||
|
$user->setUsername($keycloakUser->getUsername());
|
||||||
|
$user->setFirstName($keycloakUser->toArray()['given_name']);
|
||||||
|
$user->setName($keycloakUser->toArray()['family_name']);
|
||||||
|
$user->setEmail($email);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->setKeycloakId($keycloakUser->getId());
|
||||||
|
$this->entityManager->persist($user);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||||
|
{
|
||||||
|
// Redirect to your homepage or dashboard after login
|
||||||
|
return new RedirectResponse($this->router->generate('app_home'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||||
|
{
|
||||||
|
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
|
||||||
|
return new Response($message, Response::HTTP_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function start(Request $request, AuthenticationException $authException = null): Response
|
||||||
|
{
|
||||||
|
return new RedirectResponse($this->router->generate('connect_keycloak_start'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ class MailerSubscriber
|
|||||||
$this->mailer->send(
|
$this->mailer->send(
|
||||||
$account->getEmail(),
|
$account->getEmail(),
|
||||||
$account->getUsername(),
|
$account->getUsername(),
|
||||||
"[CRM] - Création d'un compte administrateur",
|
"[LudikEvent] - Création d'un compte administrateur",
|
||||||
"mails/new_admin.twig",
|
"mails/new_admin.twig",
|
||||||
[
|
[
|
||||||
'username' => $account->getUsername(),
|
'username' => $account->getUsername(),
|
||||||
|
|||||||
12
symfony.lock
12
symfony.lock
@@ -50,6 +50,18 @@
|
|||||||
"knplabs/knp-paginator-bundle": {
|
"knplabs/knp-paginator-bundle": {
|
||||||
"version": "v6.10.0"
|
"version": "v6.10.0"
|
||||||
},
|
},
|
||||||
|
"knpuniversity/oauth2-client-bundle": {
|
||||||
|
"version": "2.20",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes-contrib",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.20",
|
||||||
|
"ref": "1ff300d8c030f55c99219cc55050b97a695af3f6"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/knpu_oauth2_client.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
"league/flysystem-bundle": {
|
"league/flysystem-bundle": {
|
||||||
"version": "3.6",
|
"version": "3.6",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<title>{% block title %}Accueil{% endblock %}</title>
|
<title>Ludikevent | {% block title %}Accueil{% endblock %}</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
|||||||
5
templates/dashboard/administrateur.twig
Normal file
5
templates/dashboard/administrateur.twig
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{% extends 'dashboard/base.twig' %}
|
||||||
|
{% block title %}Administrateur{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
{{ dump(admins) }}
|
||||||
|
{% endblock %}
|
||||||
183
templates/dashboard/base.twig
Normal file
183
templates/dashboard/base.twig
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Tableau de Bord Administratif</title>
|
||||||
|
<!-- Chargement du CDN de Tailwind CSS -->
|
||||||
|
{{ vite_asset('admin.js',{}) }}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Configuration de la police Inter (utilisée par défaut par Tailwind) */
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
/* Style pour les cartes (utilisé pour l'effet de survol) */
|
||||||
|
.dashboard-card {
|
||||||
|
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.dashboard-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles spécifiques pour le mode sombre pour une meilleure clarté */
|
||||||
|
.dark .bg-gray-50 { background-color: #111827; }
|
||||||
|
.dark .bg-white { background-color: #1f2937; }
|
||||||
|
.dark .text-gray-800 { color: #f3f4f6; }
|
||||||
|
.dark .text-gray-900 { color: #ffffff; }
|
||||||
|
.dark .text-gray-500 { color: #9ca3af; }
|
||||||
|
.dark .border-gray-200 { border-color: #374151; }
|
||||||
|
.dark .shadow-sm { box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.5); }
|
||||||
|
.dark .shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.05); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 transition-colors duration-300">
|
||||||
|
|
||||||
|
<div class="flex h-screen">
|
||||||
|
<!-- 1. Barre Latérale (Sidebar) -->
|
||||||
|
<aside id="sidebar" class="fixed inset-y-0 left-0 z-30 w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transform -translate-x-full lg:translate-x-0 transition-transform duration-300 ease-in-out">
|
||||||
|
<div class="flex items-center justify-center h-16 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<span class="text-2xl font-bold text-primary-500">Tableau de Bord</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Liens de Navigation -->
|
||||||
|
<nav class="flex flex-col p-4 space-y-2">
|
||||||
|
<a href="{{ path('app_crm') }}" class="flex items-center space-x-3 p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out">
|
||||||
|
<!-- Icône SVG pour Clients (Users/People) -->
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M7.5 14a6.5 6.5 0 100-13 6.5 6.5 0 000 13z"></path></svg>
|
||||||
|
<span>Tableau de bord</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Lien: Clients -->
|
||||||
|
<a href="#" class="flex items-center space-x-3 p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out">
|
||||||
|
<!-- Icône SVG pour Clients (Users/People) -->
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M7.5 14a6.5 6.5 0 100-13 6.5 6.5 0 000 13z"></path></svg>
|
||||||
|
<span>Clients</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Lien: Contrats de location -->
|
||||||
|
<a href="#" class="flex items-center space-x-3 p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out">
|
||||||
|
<!-- Icône SVG pour Contrats (Documents/Paper) -->
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
||||||
|
<span>Contrats de location</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="flex items-center space-x-3 p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out">
|
||||||
|
<!-- Icône SVG pour Clients (Users/People) -->
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M7.5 14a6.5 6.5 0 100-13 6.5 6.5 0 000 13z"></path></svg>
|
||||||
|
<span>Articles</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#" class="flex items-center space-x-3 p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out">
|
||||||
|
<!-- Icône SVG pour Clients (Users/People) -->
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M7.5 14a6.5 6.5 0 100-13 6.5 6.5 0 000 13z"></path></svg>
|
||||||
|
<span>Contrat</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Menu Paramètres (avec sous-menus) -->
|
||||||
|
<div>
|
||||||
|
<button id="settings-toggle" class="w-full flex items-center justify-between p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out focus:outline-none">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<!-- Icône SVG pour Paramètres -->
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37a1.724 1.724 0 002.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||||
|
<span>Paramètres</span>
|
||||||
|
</div>
|
||||||
|
<!-- Icône de Chevron (pour l'état ouvert/fermé) -->
|
||||||
|
<svg id="settings-chevron" class="w-4 h-4 transform transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Sous-menu -->
|
||||||
|
<ul id="settings-submenu" class="ml-4 mt-1 space-y-1 hidden">
|
||||||
|
<li class="pl-2">
|
||||||
|
<a href="{{ path('app_crm_administrateur') }}" class="block p-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out">
|
||||||
|
Administrateur
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="pl-2">
|
||||||
|
<a href="#" class="block p-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out">
|
||||||
|
Services
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 2. Contenu Principal -->
|
||||||
|
<main class="flex-1 lg:ml-64 overflow-y-auto">
|
||||||
|
<!-- 2.1 En-tête (Header) -->
|
||||||
|
<header class="h-16 flex items-center justify-between px-6 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-20 shadow-sm dark:shadow-none">
|
||||||
|
|
||||||
|
<!-- Bouton pour ouvrir la barre latérale sur mobile -->
|
||||||
|
<button id="sidebar-toggle" class="lg:hidden text-gray-500 dark:text-gray-400 hover:text-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500 p-2 rounded-md">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h1 class="text-3xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{% block title %}{% endblock %}
|
||||||
|
</h1>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
{% block actions %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Script JavaScript pour la fonctionnalité de la barre latérale mobile et le menu déroulant des paramètres -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const toggleButton = document.getElementById('sidebar-toggle');
|
||||||
|
|
||||||
|
// Fonction pour basculer la visibilité de la barre latérale
|
||||||
|
toggleButton.addEventListener('click', () => {
|
||||||
|
sidebar.classList.toggle('-translate-x-full');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Masquer la barre latérale si on clique en dehors (sur mobile)
|
||||||
|
document.querySelector('main').addEventListener('click', () => {
|
||||||
|
if (!sidebar.classList.contains('-translate-x-full') && window.innerWidth < 1024) {
|
||||||
|
sidebar.classList.add('-translate-x-full');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assurer que la barre latérale est visible sur les grands écrans au chargement
|
||||||
|
const handleResize = () => {
|
||||||
|
if (window.innerWidth >= 1024) {
|
||||||
|
sidebar.classList.remove('-translate-x-full');
|
||||||
|
} else {
|
||||||
|
// Cacher si on passe en mobile, sauf si déjà ouvert
|
||||||
|
if (sidebar.classList.contains('lg:translate-x-0')) {
|
||||||
|
sidebar.classList.add('-translate-x-full');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
handleResize(); // Appel initial
|
||||||
|
|
||||||
|
// --- Logique du Menu Paramètres ---
|
||||||
|
const settingsToggle = document.getElementById('settings-toggle');
|
||||||
|
const settingsSubmenu = document.getElementById('settings-submenu');
|
||||||
|
const settingsChevron = document.getElementById('settings-chevron');
|
||||||
|
|
||||||
|
if (settingsToggle) {
|
||||||
|
settingsToggle.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault(); // Empêche la navigation et permet le dépliage
|
||||||
|
settingsSubmenu.classList.toggle('hidden');
|
||||||
|
settingsChevron.classList.toggle('rotate-180');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2
templates/dashboard/home.twig
Normal file
2
templates/dashboard/home.twig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{% extends 'dashboard/base.twig' %}
|
||||||
|
{% block title %}Tableau de bord{% endblock %}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div class="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-lg">
|
<div class="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-lg">
|
||||||
|
|
||||||
|
<img src="{{ asset('assets/images/logo.png') }}"/>
|
||||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||||
{{ 'security.login'|trans }}
|
{{ 'security.login'|trans }}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -59,6 +60,12 @@
|
|||||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||||
{{ 'button.sign_in'|trans }}
|
{{ 'button.sign_in'|trans }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<a href="{{ path('connect_keycloak_start') }}"
|
||||||
|
class="mt-2 group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||||
|
{{ 'button.sso'|trans }}
|
||||||
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,56 @@
|
|||||||
{% extends 'mails/base.twig' %}
|
{# base.twig - Modèle d'e-mail MJML #}
|
||||||
|
<mjml>
|
||||||
|
<mj-head>
|
||||||
|
<mj-title>{{ system.subject }}</mj-title>
|
||||||
|
<mj-attributes>
|
||||||
|
<mj-all font-family="Inter, Helvetica, Arial, sans-serif"></mj-all>
|
||||||
|
<mj-text font-size="16px" line-height="24px" color="#333333"></mj-text>
|
||||||
|
<mj-button background-color="#4A90E2" color="#ffffff" border-radius="4px" font-size="16px" padding="10px 25px"></mj-button>
|
||||||
|
</mj-attributes>
|
||||||
|
<mj-style inline="inline">
|
||||||
|
.link-style {
|
||||||
|
color: #4A90E2;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.footer-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
</mj-style>
|
||||||
|
</mj-head>
|
||||||
|
<mj-body background-color="#F2F2F2">
|
||||||
|
{# Section d'en-tête #}
|
||||||
|
<mj-section background-color="#ffffff" padding-bottom="0px">
|
||||||
|
<mj-column>
|
||||||
|
{# Logo mis à jour pour SARL SITECONSEIL #}
|
||||||
|
<mj-image src="{{ system.path }}{{ asset('assets/images/logo.png') }}" alt="Logo LudikEvent" align="center" width="150px" padding-bottom="20px"></mj-image>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
{% block content %}
|
{# Section de contenu #}
|
||||||
<mj-text>Bonjour, </mj-text>
|
<mj-section background-color="#ffffff" padding-top="0px" padding-bottom="0px">
|
||||||
|
{# Titre dynamique ajouté avant le bloc de contenu, directement dans la section #}
|
||||||
|
<mj-text font-size="20px" font-weight="bold" align="center" padding-bottom="20px">{{ system.subject }}</mj-text>
|
||||||
|
<mj-column width="100%">
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
{% if 'ROLE_CUSTOMER' in datas.account.roles %}
|
{# Section d'espacement #}
|
||||||
<mj-text>Nous avons reçu une demande de réinitialisation de mot de passe pour votre espace client.</mj-text>
|
<mj-section background-color="#ffffff" padding-top="0px" padding-bottom="20px">
|
||||||
{% else %}
|
<mj-column>
|
||||||
<mj-text>Nous avons reçu une demande de réinitialisation de mot de passe pour votre compte E-Cosplay.</mj-text>
|
<mj-spacer height="20px"></mj-spacer>
|
||||||
{% endif %}
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
<mj-text>Pour réinitialiser votre mot de passe, veuillez cliquer sur le bouton ci-dessous. Ce lien est valable pour une durée limitée.</mj-text>
|
{# Section de pied de page #}
|
||||||
|
<mj-section background-color="#F2F2F2" padding-top="20px" padding-bottom="20px">
|
||||||
<mj-button href="{{ datas.resetLink }}">
|
<mj-column>
|
||||||
Réinitialiser mon mot de passe
|
<mj-text align="center" css-class="footer-text">
|
||||||
</mj-button>
|
© {{ "now"|date("Y") }} LudikEvent. Tous droits réservés.
|
||||||
|
|
||||||
<mj-text padding-top="20px">
|
|
||||||
Ce lien expirera le {{ datas.request.expiresAt|date('d/m/Y à H:i') }}.
|
|
||||||
<br/>
|
|
||||||
Veuillez l'utiliser avant cette date et heure.
|
|
||||||
</mj-text>
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
<mj-text>Si vous n'avez pas demandé cette réinitialisation de mot de passe, veuillez ignorer cet e-mail. Votre mot de passe actuel restera inchangé.</mj-text>
|
</mj-section>
|
||||||
|
</mj-body>
|
||||||
<mj-text padding-top="20px">Cordialement,</mj-text>
|
</mjml>
|
||||||
<mj-text>L'équipe E-Cosplay</mj-text>
|
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@@ -25,6 +25,6 @@
|
|||||||
<br/><br/>
|
<br/><br/>
|
||||||
Cordialement,
|
Cordialement,
|
||||||
<br/>
|
<br/>
|
||||||
L'équipe CRM
|
L'équipe LudikEvent
|
||||||
</mj-text>
|
</mj-text>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div class="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-lg">
|
<div class="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-lg">
|
||||||
|
<img src="{{ asset('assets/images/logo.png') }}"/>
|
||||||
|
|
||||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||||
{{ 'events.reset_password'|trans }}
|
{{ 'events.reset_password'|trans }}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div class="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-lg">
|
<div class="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-lg">
|
||||||
|
<img src="{{ asset('assets/images/logo.png') }}"/>
|
||||||
|
|
||||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||||
{{ 'events.forgot_password'|trans }}
|
{{ 'events.forgot_password'|trans }}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div class="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-lg text-center">
|
<div class="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-lg text-center">
|
||||||
|
<img src="{{ asset('assets/images/logo.png') }}"/>
|
||||||
|
|
||||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||||
{{ 'events.reset_email_sent'|trans }}
|
{{ 'events.reset_email_sent'|trans }}
|
||||||
|
|||||||
13
templates/txt-mails/base.twig
Normal file
13
templates/txt-mails/base.twig
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[LudikEvent] - {{ system.subject }}
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Si vous ne parvenez pas à cliquer sur un lien dans cet e-mail, veuillez copier et coller l'URL dans la barre d'adresse de votre navigateur.
|
||||||
|
|
||||||
|
---
|
||||||
|
© {{ "now"|date("Y") }} LudikEvent. Tous droits réservés.
|
||||||
25
templates/txt-mails/new_admin.twig
Normal file
25
templates/txt-mails/new_admin.twig
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% extends 'txt-mails/base.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
Bonjour,
|
||||||
|
|
||||||
|
Nous avons le plaisir de vous informer que votre compte administrateur a été créé.
|
||||||
|
|
||||||
|
Voici vos identifiants de connexion temporaires :
|
||||||
|
--------------------------------------------------
|
||||||
|
Nom d'utilisateur : {{ datas.username }}
|
||||||
|
Mot de passe : {{ datas.password }}
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
Pour des raisons de sécurité, nous vous demandons de bien vouloir modifier votre mot de passe lors de votre première connexion.
|
||||||
|
|
||||||
|
Vous pouvez vous connecter à votre compte en utilisant le lien ci-dessous :
|
||||||
|
|
||||||
|
Lien de connexion : {{ system.path }}{{ datas.url }}
|
||||||
|
|
||||||
|
Si vous avez des questions ou rencontrez des difficultés, n'hésitez pas à nous contacter.
|
||||||
|
|
||||||
|
Cordialement,
|
||||||
|
|
||||||
|
L'équipe LudikEvent
|
||||||
|
{% endblock %}
|
||||||
@@ -28,3 +28,4 @@ logged_in_as: Connecté en tant que
|
|||||||
logout_link: Déconnexion
|
logout_link: Déconnexion
|
||||||
page.login: Connexion
|
page.login: Connexion
|
||||||
logged_admin: Administration
|
logged_admin: Administration
|
||||||
|
button.sso: Connexion SSO
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ GREEN='\033[0;32m'
|
|||||||
CYAN='\033[0;36m'
|
CYAN='\033[0;36m'
|
||||||
RESET='\033[0m' # Reset color to default
|
RESET='\033[0m' # Reset color to default
|
||||||
|
|
||||||
echo "${CYAN}#######################${RESET}"
|
echo "${CYAN}####################################${RESET}"
|
||||||
echo "${CYAN}# E-PAGE UPDATE START #${RESET}"
|
echo "${CYAN}# LUDIKEVENT INTRANET UPDATE START #${RESET}"
|
||||||
echo "${CYAN}#######################${RESET}"
|
echo "${CYAN}####################################${RESET}"
|
||||||
ansible-playbook -i ansible/hosts.ini ansible/playbook.yml
|
ansible-playbook -i ansible/hosts.ini ansible/playbook.yml
|
||||||
echo "${CYAN}##############${RESET}"
|
echo "${CYAN}##############${RESET}"
|
||||||
echo "${CYAN}# END UPDATE #${RESET}"
|
echo "${CYAN}# END UPDATE #${RESET}"
|
||||||
|
|||||||
Reference in New Issue
Block a user