Compare commits
1 Commits
dev/webhoo
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
400aeedb8f |
|
|
@ -1,49 +0,0 @@
|
||||||
# Git files
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
.gitattributes
|
|
||||||
|
|
||||||
# IDE files
|
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
README.md
|
|
||||||
*.MD
|
|
||||||
HELPER.MD
|
|
||||||
NOTIFICATION.MD
|
|
||||||
|
|
||||||
# Test files
|
|
||||||
tests
|
|
||||||
phpunit.xml.dist
|
|
||||||
|
|
||||||
# CI/CD
|
|
||||||
.github
|
|
||||||
.gitlab-ci.yml
|
|
||||||
|
|
||||||
# Platform.sh
|
|
||||||
.platform
|
|
||||||
.platform.app.yaml
|
|
||||||
|
|
||||||
# Docker files (don't copy Docker files into the image)
|
|
||||||
Dockerfile
|
|
||||||
docker-compose*.yml
|
|
||||||
compose*.yaml
|
|
||||||
.dockerignore
|
|
||||||
|
|
||||||
# Environment files (will be provided at runtime)
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
|
|
||||||
# Development dependencies
|
|
||||||
.php-cs-fixer.cache
|
|
||||||
|
|
||||||
# Cache and logs (will be created at runtime)
|
|
||||||
var/cache/*
|
|
||||||
var/log/*
|
|
||||||
var/sessions/*
|
|
||||||
|
|
||||||
# Vendor (will be installed during build)
|
|
||||||
# vendor is installed during the build process, so we don't ignore it
|
|
||||||
19
.env
|
|
@ -17,7 +17,6 @@
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
APP_ENV=prod
|
APP_ENV=prod
|
||||||
APP_SECRET='kjuusshgvk35434judshfgvkusd224444hvg'
|
APP_SECRET='kjuusshgvk35434judshfgvkusd224444hvg'
|
||||||
APPLICATION=EasyPortal
|
|
||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
###> doctrine/doctrine-bundle ###
|
###> doctrine/doctrine-bundle ###
|
||||||
|
|
@ -44,12 +43,10 @@ MAILER_DSN=null://null
|
||||||
TRUSTED_PROXY='185.116.130.121','10.8.34.21'
|
TRUSTED_PROXY='185.116.130.121','10.8.34.21'
|
||||||
|
|
||||||
###> league/oauth2-server-bundle ###
|
###> league/oauth2-server-bundle ###
|
||||||
OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.key
|
OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||||
OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.key
|
OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||||
OAUTH_PASSPHRASE=8170ea18d2e3e05b5c7ae0672a754bf4
|
OAUTH_PASSPHRASE=8170ea18d2e3e05b5c7ae0672a754bf4
|
||||||
OAUTH_ENCRYPTION_KEY=f1b7c279f7992205a0df45e295d07066
|
OAUTH_ENCRYPTION_KEY=f1b7c279f7992205a0df45e295d07066
|
||||||
OAUTH_SSO_IDENTIFIER='sso-own-identifier'
|
|
||||||
OAUTH_SSO_IDENTIFIER_LOGIN='sso-own-identifier'
|
|
||||||
###< league/oauth2-server-bundle ###
|
###< league/oauth2-server-bundle ###
|
||||||
|
|
||||||
###> nelmio/cors-bundle ###
|
###> nelmio/cors-bundle ###
|
||||||
|
|
@ -65,15 +62,3 @@ MERCURE_PUBLIC_URL=https://example.com/.well-known/mercure
|
||||||
# The secret used to sign the JWTs
|
# The secret used to sign the JWTs
|
||||||
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
|
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
|
||||||
###< symfony/mercure-bundle ###
|
###< symfony/mercure-bundle ###
|
||||||
|
|
||||||
###> aws/aws-sdk-php-symfony ###
|
|
||||||
AWS_KEY=not-a-real-key
|
|
||||||
AWS_SECRET=@@not-a-real-secret
|
|
||||||
AWS_REGION=us-east-1
|
|
||||||
AWS_ENDPOINT=https://s3.amazonaws.com
|
|
||||||
AWS_S3_PORTAL_URL=https://s3.amazonaws.com/portal
|
|
||||||
###< aws/aws-sdk-php-symfony ###
|
|
||||||
APP_URL='https://example.com'
|
|
||||||
APP_DOMAIN='example.com'
|
|
||||||
|
|
||||||
EASYCHECK_URL='https://testcheck.solutions-easy.com'
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
APP_SECRET=bba5a2c490c3f92030618ecb97b5138e
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
# define your env variables for the test env here
|
||||||
|
KERNEL_CLASS='App\Kernel'
|
||||||
|
APP_SECRET='$ecretf0rt3st'
|
||||||
|
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||||
|
PANTHER_APP_ENV=panther
|
||||||
|
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
/.env.local
|
/.env.local
|
||||||
/.env.test
|
|
||||||
/.env.local.php
|
/.env.local.php
|
||||||
/.env.*.local
|
/.env.*.local
|
||||||
/config/secrets/prod/prod.decrypt.private.php
|
/config/secrets/prod/prod.decrypt.private.php
|
||||||
/public/bundles/
|
/public/bundles/
|
||||||
/public/uploads/
|
|
||||||
/var/
|
/var/
|
||||||
/vendor/
|
/vendor/
|
||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
|
@ -16,18 +14,12 @@
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
###< phpunit/phpunit ###
|
###< phpunit/phpunit ###
|
||||||
|
|
||||||
|
###> symfony/phpunit-bridge ###
|
||||||
|
.phpunit.result.cache
|
||||||
|
/phpunit.xml
|
||||||
|
###< symfony/phpunit-bridge ###
|
||||||
|
|
||||||
###> symfony/asset-mapper ###
|
###> symfony/asset-mapper ###
|
||||||
/public/assets/
|
/public/assets/
|
||||||
/assets/vendor/
|
/assets/vendor/
|
||||||
###< symfony/asset-mapper ###
|
###< symfony/asset-mapper ###
|
||||||
|
|
||||||
###> IntelliJ IDEA ###
|
|
||||||
.idea/
|
|
||||||
*.iml
|
|
||||||
###< IntelliJ IDEA ###
|
|
||||||
|
|
||||||
###>jwt keys###
|
|
||||||
/config/jwt/private.key
|
|
||||||
/config/jwt/public.key
|
|
||||||
###<jwt keys###
|
|
||||||
/composer.lock
|
|
||||||
|
|
|
||||||
302
.gitlab-ci.yml
|
|
@ -1,302 +0,0 @@
|
||||||
stages:
|
|
||||||
- build
|
|
||||||
- deploy
|
|
||||||
- notify
|
|
||||||
|
|
||||||
variables:
|
|
||||||
# OVH Container Registry URL (without https://)
|
|
||||||
# Image name and tag (format: registry/project/image)
|
|
||||||
IMAGE_NAME: "easyportal"
|
|
||||||
IMAGE_TAG: "${OVH_REGISTRY_URL}/${IMAGE_NAME}:${CI_COMMIT_REF_SLUG}-${CI_PIPELINE_ID}"
|
|
||||||
IMAGE_LATEST: "${OVH_REGISTRY_URL}/${IMAGE_NAME}:latest"
|
|
||||||
|
|
||||||
build:
|
|
||||||
stage: build
|
|
||||||
image: docker:20.10.16
|
|
||||||
rules:
|
|
||||||
- if: '$CI_COMMIT_BRANCH == "develop"'
|
|
||||||
when: always
|
|
||||||
- when: manual
|
|
||||||
before_script:
|
|
||||||
# Use host Docker daemon via socket
|
|
||||||
- docker info
|
|
||||||
# Login to OVH Container Registry
|
|
||||||
- echo "Logging into OVH Container Registry..."
|
|
||||||
- echo "$OVH_REGISTRY_PASSWORD" | docker login -u "$OVH_REGISTRY_USERNAME" "$OVH_REGISTRY_URL" --password-stdin
|
|
||||||
script:
|
|
||||||
# Build the FrankenPHP image
|
|
||||||
- echo "Building Docker image..."
|
|
||||||
- docker build --build-arg APP_ENV=prod --target frankenphp_prod -t $IMAGE_TAG -t $IMAGE_LATEST .
|
|
||||||
|
|
||||||
# Push both tags to OVH registry
|
|
||||||
- echo "Pushing image to OVH registry..."
|
|
||||||
- docker push $IMAGE_TAG
|
|
||||||
- docker push $IMAGE_LATEST
|
|
||||||
|
|
||||||
# Display image info
|
|
||||||
- echo "Successfully pushed:"
|
|
||||||
- echo " - $IMAGE_TAG"
|
|
||||||
- echo " - $IMAGE_LATEST"
|
|
||||||
after_script:
|
|
||||||
- docker logout $OVH_REGISTRY_URL || true
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
stage: deploy
|
|
||||||
image: alpine:latest
|
|
||||||
needs:
|
|
||||||
- build
|
|
||||||
rules:
|
|
||||||
- if: '$CI_COMMIT_BRANCH == "develop"'
|
|
||||||
when: on_success
|
|
||||||
- when: manual
|
|
||||||
before_script:
|
|
||||||
- apk add --no-cache openssh-client
|
|
||||||
- mkdir -p ~/.ssh
|
|
||||||
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
|
||||||
- chmod 600 ~/.ssh/id_rsa
|
|
||||||
- echo "Host *\n StrictHostKeyChecking no\n UserKnownHostsFile=/dev/null" > ~/.ssh/config
|
|
||||||
- chmod 600 ~/.ssh/config
|
|
||||||
- ssh-keyscan -H $SERVER_IP >> ~/.ssh/known_hosts || true
|
|
||||||
script:
|
|
||||||
- |
|
|
||||||
ssh $SSH_USER@$SERVER_IP << ENDSSH
|
|
||||||
set -e
|
|
||||||
cd /mnt/external-disk/easy-monitor/easyportal
|
|
||||||
|
|
||||||
# ===== PRE-DEPLOYMENT DOCKER CLEANUP =====
|
|
||||||
echo '===== Starting pre-deployment Docker cleanup ====='
|
|
||||||
|
|
||||||
# Show current Docker space usage
|
|
||||||
echo 'Current Docker space usage:'
|
|
||||||
docker system df
|
|
||||||
|
|
||||||
# Clean build cache
|
|
||||||
echo 'Cleaning Docker build cache...'
|
|
||||||
docker builder prune -f || true
|
|
||||||
|
|
||||||
# Clean stopped containers
|
|
||||||
echo 'Cleaning stopped containers...'
|
|
||||||
docker container prune -f || true
|
|
||||||
|
|
||||||
# Clean unused networks
|
|
||||||
echo 'Cleaning unused networks...'
|
|
||||||
docker network prune -f || true
|
|
||||||
|
|
||||||
# Show space usage after cleanup
|
|
||||||
echo 'Docker space usage after initial cleanup:'
|
|
||||||
docker system df
|
|
||||||
|
|
||||||
# Login to OVH registry
|
|
||||||
echo "$OVH_REGISTRY_PASSWORD" | docker login -u "$OVH_REGISTRY_USERNAME" "$OVH_REGISTRY_URL" --password-stdin
|
|
||||||
|
|
||||||
# Pull latest image
|
|
||||||
echo 'Pulling latest image...'
|
|
||||||
docker pull $IMAGE_LATEST
|
|
||||||
|
|
||||||
# Clean unused images before deployment
|
|
||||||
echo 'Cleaning unused Docker images...'
|
|
||||||
docker image prune -f || true
|
|
||||||
|
|
||||||
# Update .env.compose with new image tag
|
|
||||||
echo 'Updating .env.compose with new image...'
|
|
||||||
echo "EASYPORTAL_IMAGE=$IMAGE_LATEST" > .env.compose
|
|
||||||
|
|
||||||
# Recreate php and messenger containers with new image
|
|
||||||
echo 'Restarting EasyPortal services with new image...'
|
|
||||||
docker compose --env-file .env.compose up -d --force-recreate --no-deps php messenger
|
|
||||||
|
|
||||||
# Wait for service to be ready
|
|
||||||
sleep 15
|
|
||||||
|
|
||||||
# Run migrations
|
|
||||||
echo 'Running database migrations...'
|
|
||||||
docker compose --env-file .env.compose exec -T php php bin/console doctrine:migrations:migrate --no-interaction
|
|
||||||
|
|
||||||
# Clear cache
|
|
||||||
echo 'Clearing Symfony cache...'
|
|
||||||
docker compose --env-file .env.compose exec -T php php bin/console cache:clear --env=prod --no-debug
|
|
||||||
|
|
||||||
# ===== POST-DEPLOYMENT CLEANUP =====
|
|
||||||
echo '===== Final Docker cleanup ====='
|
|
||||||
|
|
||||||
docker builder prune -f || true
|
|
||||||
docker container prune -f || true
|
|
||||||
|
|
||||||
# Keep last 3 image versions
|
|
||||||
echo 'Cleaning old image versions (keeping last 3)...'
|
|
||||||
docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedAt}}' | grep -v REPOSITORY | sort -k1,1 -k4,4r | awk '
|
|
||||||
{
|
|
||||||
repo_tag = \$1":" \$2
|
|
||||||
if (repo_count[\$1]++ >= 3 && \$2 != "latest" && \$1 != "<none>") {
|
|
||||||
print \$3
|
|
||||||
}
|
|
||||||
}' | xargs -r docker rmi -f || true
|
|
||||||
|
|
||||||
echo 'Final Docker space usage:'
|
|
||||||
docker system df
|
|
||||||
|
|
||||||
echo '===== Deployment completed successfully! ====='
|
|
||||||
ENDSSH
|
|
||||||
after_script:
|
|
||||||
- rm -f ~/.ssh/id_rsa
|
|
||||||
environment:
|
|
||||||
name: production
|
|
||||||
url: https://testportail.solutions-easy.com
|
|
||||||
|
|
||||||
notify_success:
|
|
||||||
stage: notify
|
|
||||||
image: curlimages/curl:latest
|
|
||||||
needs:
|
|
||||||
- deploy
|
|
||||||
rules:
|
|
||||||
- when: on_success
|
|
||||||
- when: manual
|
|
||||||
allow_failure: true
|
|
||||||
script:
|
|
||||||
- |
|
|
||||||
COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | cut -c1-8)
|
|
||||||
DEPLOYMENT_TYPE="Déploiement Beta"
|
|
||||||
|
|
||||||
if [ "$CI_COMMIT_BRANCH" != "develop" ]; then
|
|
||||||
DEPLOYMENT_TYPE="Déploiement Beta"
|
|
||||||
fi
|
|
||||||
|
|
||||||
curl -H "Content-Type: application/json" -d "{
|
|
||||||
\"type\": \"message\",
|
|
||||||
\"attachments\": [
|
|
||||||
{
|
|
||||||
\"contentType\": \"application/vnd.microsoft.card.adaptive\",
|
|
||||||
\"content\": {
|
|
||||||
\"type\": \"AdaptiveCard\",
|
|
||||||
\"body\": [
|
|
||||||
{
|
|
||||||
\"type\": \"TextBlock\",
|
|
||||||
\"text\": \"$DEPLOYMENT_TYPE\",
|
|
||||||
\"weight\": \"Bolder\",
|
|
||||||
\"size\": \"Large\"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\"type\": \"TextBlock\",
|
|
||||||
\"text\": \"**App:** EasyPortal\",
|
|
||||||
\"wrap\": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\"type\": \"TextBlock\",
|
|
||||||
\"text\": \"**Version:** [$CI_COMMIT_REF_NAME - $COMMIT_SHORT_SHA]($CI_PROJECT_URL/-/commit/$CI_COMMIT_SHA)\",
|
|
||||||
\"wrap\": true,
|
|
||||||
\"markdown\": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\"type\": \"TextBlock\",
|
|
||||||
\"text\": \"**Pipeline:** [Voir le pipeline]($CI_PIPELINE_URL)\",
|
|
||||||
\"wrap\": true,
|
|
||||||
\"markdown\": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\"type\": \"TextBlock\",
|
|
||||||
\"text\": \"**Auteur:** $GITLAB_USER_LOGIN\",
|
|
||||||
\"wrap\": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\"type\": \"TextBlock\",
|
|
||||||
\"text\": \"**Statut:** Succès ✓\",
|
|
||||||
\"wrap\": true,
|
|
||||||
\"color\": \"Good\"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
\"actions\": [
|
|
||||||
{
|
|
||||||
\"type\": \"Action.OpenUrl\",
|
|
||||||
\"title\": \"Voir le pipeline\",
|
|
||||||
\"url\": \"$CI_PIPELINE_URL\"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\"type\": \"Action.OpenUrl\",
|
|
||||||
\"title\": \"Voir le commit\",
|
|
||||||
\"url\": \"$CI_PROJECT_URL/-/commit/$CI_COMMIT_SHA\"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
\"\$schema\": \"http://adaptivecards.io/schemas/adaptive-card.json\",
|
|
||||||
\"version\": \"1.0\"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}" "$TEAMS_WEBHOOK_URL"
|
|
||||||
|
|
||||||
notify_failure:
|
|
||||||
stage: notify
|
|
||||||
image: curlimages/curl:latest
|
|
||||||
needs:
|
|
||||||
- deploy
|
|
||||||
rules:
|
|
||||||
- when: on_failure
|
|
||||||
script:
|
|
||||||
- |
|
|
||||||
COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | cut -c1-8)
|
|
||||||
DEPLOYMENT_TYPE="Déploiement Beta"
|
|
||||||
|
|
||||||
if [ "$CI_COMMIT_BRANCH" != "develop" ]; then
|
|
||||||
DEPLOYMENT_TYPE="Déploiement Beta"
|
|
||||||
fi
|
|
||||||
|
|
||||||
curl -H "Content-Type: application/json" -d "{
|
|
||||||
\"type\": \"message\",
|
|
||||||
\"attachments\": [
|
|
||||||
{
|
|
||||||
\"contentType\": \"application/vnd.microsoft.card.adaptive\",
|
|
||||||
\"content\": {
|
|
||||||
\"type\": \"AdaptiveCard\",
|
|
||||||
\"body\": [
|
|
||||||
{
|
|
||||||
\"type\": \"TextBlock\",
|
|
||||||
\"text\": \"$DEPLOYMENT_TYPE\",
|
|
||||||
\"weight\": \"Bolder\",
|
|
||||||
\"size\": \"Large\"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\"type\": \"TextBlock\",
|
|
||||||
\"text\": \"**App:** EasyPortal\",
|
|
||||||
\"wrap\": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\"type\": \"TextBlock\",
|
|
||||||
\"text\": \"**Version:** [$CI_COMMIT_REF_NAME - $COMMIT_SHORT_SHA]($CI_PROJECT_URL/-/commit/$CI_COMMIT_SHA)\",
|
|
||||||
\"wrap\": true,
|
|
||||||
\"markdown\": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\"type\": \"TextBlock\",
|
|
||||||
\"text\": \"**Pipeline:** [Voir le pipeline]($CI_PIPELINE_URL)\",
|
|
||||||
\"wrap\": true,
|
|
||||||
\"markdown\": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\"type\": \"TextBlock\",
|
|
||||||
\"text\": \"**Auteur:** $GITLAB_USER_LOGIN\",
|
|
||||||
\"wrap\": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\"type\": \"TextBlock\",
|
|
||||||
\"text\": \"**Statut:** Échec ✗\",
|
|
||||||
\"wrap\": true,
|
|
||||||
\"color\": \"Attention\"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
\"actions\": [
|
|
||||||
{
|
|
||||||
\"type\": \"Action.OpenUrl\",
|
|
||||||
\"title\": \"Voir le pipeline\",
|
|
||||||
\"url\": \"$CI_PIPELINE_URL\"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
\"type\": \"Action.OpenUrl\",
|
|
||||||
\"title\": \"Voir le commit\",
|
|
||||||
\"url\": \"$CI_PROJECT_URL/-/commit/$CI_COMMIT_SHA\"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
\"\$schema\": \"http://adaptivecards.io/schemas/adaptive-card.json\",
|
|
||||||
\"version\": \"1.0\"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}" "$TEAMS_WEBHOOK_URL"
|
|
||||||
|
|
@ -8,23 +8,8 @@
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure-bundle" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mercure-bundle" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/firebase/php-jwt" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/firebase/php-jwt" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/aws/aws-crt-php" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/aws/aws-sdk-php" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/aws/aws-sdk-php-symfony" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/promises" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/knplabs/knp-time-bundle" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/mtdowling/jmespath.php" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/rate-limiter" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/remote-event" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/webhook" />
|
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="library" name="quill.snow" level="application" />
|
|
||||||
<orderEntry type="library" name="quill" level="application" />
|
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
|
|
@ -10,11 +10,6 @@
|
||||||
<option name="highlightLevel" value="WARNING" />
|
<option name="highlightLevel" value="WARNING" />
|
||||||
<option name="transferred" value="true" />
|
<option name="transferred" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpCodeSniffer">
|
|
||||||
<phpcs_settings>
|
|
||||||
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="e61c7a46-7290-4f1b-ace7-5462f6da9ae0" timeout="30000" />
|
|
||||||
</phpcs_settings>
|
|
||||||
</component>
|
|
||||||
<component name="PhpIncludePathManager">
|
<component name="PhpIncludePathManager">
|
||||||
<include_path>
|
<include_path>
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
||||||
|
|
@ -88,6 +83,7 @@
|
||||||
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
||||||
<path value="$PROJECT_DIR$/vendor/lcobucci/clock" />
|
<path value="$PROJECT_DIR$/vendor/lcobucci/clock" />
|
||||||
<path value="$PROJECT_DIR$/vendor/twig/twig" />
|
<path value="$PROJECT_DIR$/vendor/twig/twig" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
|
||||||
<path value="$PROJECT_DIR$/vendor/composer" />
|
<path value="$PROJECT_DIR$/vendor/composer" />
|
||||||
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
||||||
|
|
@ -145,6 +141,7 @@
|
||||||
<path value="$PROJECT_DIR$/vendor/defuse/php-encryption" />
|
<path value="$PROJECT_DIR$/vendor/defuse/php-encryption" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/ux-turbo" />
|
<path value="$PROJECT_DIR$/vendor/symfony/ux-turbo" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/stimulus-bundle" />
|
<path value="$PROJECT_DIR$/vendor/symfony/stimulus-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/cache" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
|
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
||||||
|
|
@ -167,32 +164,9 @@
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/mercure" />
|
<path value="$PROJECT_DIR$/vendor/symfony/mercure" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/mercure-bundle" />
|
<path value="$PROJECT_DIR$/vendor/symfony/mercure-bundle" />
|
||||||
<path value="$PROJECT_DIR$/vendor/firebase/php-jwt" />
|
<path value="$PROJECT_DIR$/vendor/firebase/php-jwt" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/cache" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/knplabs/knp-time-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/aws/aws-sdk-php" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/aws/aws-crt-php" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/aws/aws-sdk-php-symfony" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/guzzlehttp/promises" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/mtdowling/jmespath.php" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/rate-limiter" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/dama/doctrine-test-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/remote-event" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/webhook" />
|
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
|
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
|
||||||
<component name="PhpStan">
|
|
||||||
<PhpStan_settings>
|
|
||||||
<phpstan_by_interpreter asDefaultInterpreter="true" interpreter_id="e61c7a46-7290-4f1b-ace7-5462f6da9ae0" timeout="60000" />
|
|
||||||
</PhpStan_settings>
|
|
||||||
</component>
|
|
||||||
<component name="PhpStanOptionsConfiguration">
|
<component name="PhpStanOptionsConfiguration">
|
||||||
<option name="transferred" value="true" />
|
<option name="transferred" value="true" />
|
||||||
</component>
|
</component>
|
||||||
|
|
@ -201,11 +175,6 @@
|
||||||
<PhpUnitSettings configuration_file_path="$PROJECT_DIR$/phpunit.xml.dist" custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" use_configuration_file="true" />
|
<PhpUnitSettings configuration_file_path="$PROJECT_DIR$/phpunit.xml.dist" custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" use_configuration_file="true" />
|
||||||
</phpunit_settings>
|
</phpunit_settings>
|
||||||
</component>
|
</component>
|
||||||
<component name="Psalm">
|
|
||||||
<Psalm_settings>
|
|
||||||
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="e61c7a46-7290-4f1b-ace7-5462f6da9ae0" timeout="60000" />
|
|
||||||
</Psalm_settings>
|
|
||||||
</component>
|
|
||||||
<component name="PsalmOptionsConfiguration">
|
<component name="PsalmOptionsConfiguration">
|
||||||
<option name="transferred" value="true" />
|
<option name="transferred" value="true" />
|
||||||
</component>
|
</component>
|
||||||
|
|
|
||||||
104
Dockerfile
|
|
@ -1,104 +0,0 @@
|
||||||
#syntax=docker/dockerfile:1
|
|
||||||
|
|
||||||
# Versions
|
|
||||||
FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream
|
|
||||||
|
|
||||||
# The different stages of this Dockerfile are meant to be built into separate images
|
|
||||||
# https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage
|
|
||||||
# https://docs.docker.com/compose/compose-file/#target
|
|
||||||
|
|
||||||
|
|
||||||
# Base FrankenPHP image
|
|
||||||
FROM frankenphp_upstream AS frankenphp_base
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
VOLUME /app/var/
|
|
||||||
|
|
||||||
# persistent / runtime deps
|
|
||||||
# hadolint ignore=DL3008
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
acl \
|
|
||||||
file \
|
|
||||||
gettext \
|
|
||||||
git \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
install-php-extensions \
|
|
||||||
@composer \
|
|
||||||
apcu \
|
|
||||||
intl \
|
|
||||||
opcache \
|
|
||||||
zip \
|
|
||||||
;
|
|
||||||
|
|
||||||
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
|
|
||||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
|
||||||
|
|
||||||
# Transport to use by Mercure (default to Bolt)
|
|
||||||
ENV MERCURE_TRANSPORT_URL=bolt:///data/mercure.db
|
|
||||||
|
|
||||||
ENV PHP_INI_SCAN_DIR=":$PHP_INI_DIR/app.conf.d"
|
|
||||||
|
|
||||||
###> recipes ###
|
|
||||||
###> doctrine/doctrine-bundle ###
|
|
||||||
RUN install-php-extensions pdo_pgsql
|
|
||||||
###< doctrine/doctrine-bundle ###
|
|
||||||
###< recipes ###
|
|
||||||
|
|
||||||
COPY --link frankenphp/conf.d/10-app.ini $PHP_INI_DIR/app.conf.d/
|
|
||||||
COPY --link --chmod=755 frankenphp/docker-entrypoint.sh /usr/local/bin/docker-entrypoint
|
|
||||||
COPY --link frankenphp/Caddyfile.prod /etc/frankenphp/Caddyfile
|
|
||||||
|
|
||||||
ENTRYPOINT ["docker-entrypoint"]
|
|
||||||
|
|
||||||
HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1
|
|
||||||
CMD [ "frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile" ]
|
|
||||||
|
|
||||||
# Dev FrankenPHP image
|
|
||||||
FROM frankenphp_base AS frankenphp_dev
|
|
||||||
|
|
||||||
ENV APP_ENV=dev
|
|
||||||
ENV XDEBUG_MODE=off
|
|
||||||
ENV FRANKENPHP_WORKER_CONFIG=watch
|
|
||||||
|
|
||||||
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
install-php-extensions \
|
|
||||||
xdebug \
|
|
||||||
;
|
|
||||||
|
|
||||||
COPY --link frankenphp/conf.d/20-app.dev.ini $PHP_INI_DIR/app.conf.d/
|
|
||||||
|
|
||||||
CMD [ "frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile", "--watch" ]
|
|
||||||
|
|
||||||
# Prod FrankenPHP image
|
|
||||||
FROM frankenphp_base AS frankenphp_prod
|
|
||||||
|
|
||||||
ENV APP_ENV=prod
|
|
||||||
|
|
||||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
|
||||||
|
|
||||||
COPY --link frankenphp/conf.d/20-app.prod.ini $PHP_INI_DIR/app.conf.d/
|
|
||||||
|
|
||||||
# prevent the reinstallation of vendors at every changes in the source code
|
|
||||||
COPY --link composer.* symfony.* ./
|
|
||||||
RUN set -eux; \
|
|
||||||
composer install --no-cache --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress
|
|
||||||
|
|
||||||
# copy sources
|
|
||||||
COPY --link . ./
|
|
||||||
RUN rm -Rf frankenphp/
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
mkdir -p var/cache var/log; \
|
|
||||||
composer dump-autoload --classmap-authoritative --no-dev; \
|
|
||||||
composer dump-env prod; \
|
|
||||||
chmod +x bin/console; \
|
|
||||||
# Install JavaScript vendor assets and compile assets for production \
|
|
||||||
php bin/console importmap:install; \
|
|
||||||
php bin/console asset-map:compile; \
|
|
||||||
php bin/console assets:install public; \
|
|
||||||
sync;
|
|
||||||
41
HELPER.MD
|
|
@ -1,41 +0,0 @@
|
||||||
## Notes
|
|
||||||
- Certaines abbreviations sont utilisées afin de simplifier le code et d'éviter les répétitions ou noms trop longs :
|
|
||||||
- `uo` pour `User Organization`
|
|
||||||
- `uoId` pour `User Organization Id`
|
|
||||||
- `oa` pour `Organization Application`
|
|
||||||
- `at` pour `Access Token`
|
|
||||||
- A delete command is available to delete roles
|
|
||||||
|
|
||||||
|
|
||||||
### ROLES
|
|
||||||
```bash
|
|
||||||
php bin/console app:delete-role ROLE_NAME
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tabulator
|
|
||||||
- Certaines fonctions sont déjà disponibles (snippet) mais commentées, car on ne les utilise pas
|
|
||||||
- Exemples de sorting et filtering sont disponibles dans 'src/controller/organization.php' L.268
|
|
||||||
|
|
||||||
### Frontend:
|
|
||||||
- Le Body/content de chaque page sont dans des div avec le style suivant :
|
|
||||||
``` html
|
|
||||||
<div class="w-100 h-100 p-5 m-auto">
|
|
||||||
```
|
|
||||||
- L'espace entre les éléments cartes est avec l'un des styles suivants :
|
|
||||||
``` html
|
|
||||||
<div class="mb-3"> <!-- margin bottom -->
|
|
||||||
<div class="mt-3"> <!-- margin top -->
|
|
||||||
<div class="me-3"> <!-- margin end/right -->
|
|
||||||
<div class="ms-3"> <!-- margin start/left -->
|
|
||||||
<div class="mx-3"> <!-- margin left and right -->
|
|
||||||
<div class="my-3"> <!-- margin top and bottom -->
|
|
||||||
<div class="m-3"> <!-- margin -->
|
|
||||||
<div class="d-flex gap-2"> <!-- gap entre les boutons -->
|
|
||||||
```
|
|
||||||
- Chaque élément est une carte afin de donner un style uniforme :
|
|
||||||
``` html
|
|
||||||
<div class="card p-3">
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
php bin/console messenger:consume async -vv
|
|
||||||
190
NOTIFICATION.MD
|
|
@ -1,190 +0,0 @@
|
||||||
# Système de notification de l'application
|
|
||||||
## Vue d'ensemble
|
|
||||||
Le système de notification de l'application permet d'informer les utilisateurs de diverse action.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
### Composants principaux
|
|
||||||
1. **Service de Notification** : Gère la création, l'envoi et le suivi des notifications.
|
|
||||||
2. **Interface Utilisateur** : Affiche les notifications aux utilisateurs via des pops-ups, des bannières ou des emails.
|
|
||||||
3. **Template Email** : Modèles prédéfinis pour les notifications par email.
|
|
||||||
4. **Type D'action** : Enum d'action qui déclenche des notifications (ex: nouvel utilisateur, utilisateur actif, ...).
|
|
||||||
|
|
||||||
### Service de Notification
|
|
||||||
Le service de notification est responsable de la gestion des notifications. Il inclut les fonctionnalités suivantes :
|
|
||||||
- Création de notifications basées sur des événements spécifiques.
|
|
||||||
- Envoi de notifications via différents canaux (email, in-app).
|
|
||||||
- Suivi de l'état des notifications (envoyé, lu, etc.).
|
|
||||||
|
|
||||||
### Interface Utilisateur
|
|
||||||
L'interface utilisateur affiche les notifications de manière conviviale. Les notifications peuvent apparaître sous forme
|
|
||||||
de pops-ups, de bannières ou d'emails. (Possibilité d'intéragir avec les notifications)
|
|
||||||
|
|
||||||
### Template Email
|
|
||||||
Les templates email sont utilisés pour formater les notifications envoyées par email. Chaque type de notification
|
|
||||||
a son propre template pour assurer une communication claire et cohérente.
|
|
||||||
|
|
||||||
|
|
||||||
### Type d'action
|
|
||||||
```
|
|
||||||
enum ActionType: String {
|
|
||||||
case NewUser = "NEW_USER";
|
|
||||||
case ActiveUser = "ACTIVE_USER";
|
|
||||||
case PasswordReset = "PASSWORD_RESET";
|
|
||||||
case SubscriptionExpired = "SUBSCRIPTION_EXPIRED";
|
|
||||||
case OrganizationInvited = "ORGANIZATION_INVITED";
|
|
||||||
case OrganizationInactive = "ORGANIZATION_INACTIVE";
|
|
||||||
case OrganizationDeleted = "ORGANIZATION_DELETED";
|
|
||||||
case OrginizationUserInvited = "ORGANIZATION_USER_INVITED";
|
|
||||||
case UserDeleted = "USER_DELETED";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Flux de travail
|
|
||||||
1. L’administrateur crée un utilisateur depuis l’interface (formulaire “Créer un utilisateur”).
|
|
||||||
2. Le contrôleur valide la requête et appelle le cas d’usage UserAdministrationService->handle(ActionType::NewUser, $admin, $payload).
|
|
||||||
3. Le service crée l’utilisateur en base avec le statut INVITED, associe l’organisation de l’admin, et génère un lien signé/jeton de setup de mot de passe (TTL).
|
|
||||||
4. Le service publie un événement de domaine UserInvitedEvent { userId, adminId, organizationId } sur Messenger (transport async).
|
|
||||||
5. Handler async A — SendUserInvitationEmailHandler:
|
|
||||||
6. Construit l’email via Symfony Mailer + Twig (emails/user_invitation.html.twig) avec le lien de définition de mot de passe.
|
|
||||||
7. Envoie le mail à l’utilisateur invité.
|
|
||||||
8. Handler async B — NotifyAdminInvitationSentHandler:
|
|
||||||
9. Crée une notification interne (Notifier, canal “in‑app”).
|
|
||||||
10. Pousse un événement temps réel via Mercure sur le topic admin/{adminId}/events avec le type INVITATION_EMAIL_SENT.
|
|
||||||
11. L’UI admin affiche un toast/bannière confirmant “Email d’invitation envoyé”.
|
|
||||||
12. L’utilisateur ouvre l’email et clique le lien de définition de mot de passe.
|
|
||||||
13. Le PasswordSetupController vérifie la signature/le jeton et la validité (TTL), affiche le formulaire, puis enregistre le nouveau mot de passe.
|
|
||||||
14. À la réussite, l’utilisateur passe au statut ACTIVE et l’action publie UserActivatedEvent { userId, adminId, organizationId } sur Messenger (async).
|
|
||||||
15. Handler async C — NotifyAdminUserActivatedHandler:
|
|
||||||
16. Crée une notification interne (Notifier, canal “in‑app”) “Compte activé”.
|
|
||||||
17. Pousse un événement Mercure sur admin/{adminId}/events avec le type USER_ACTIVATED.
|
|
||||||
18. L’UI admin met à jour la liste des membres (badge “Actif”) et affiche un toast confirmant l’activation.
|
|
||||||
19. Journalisation/Audit:
|
|
||||||
20. Chaque handler écrit une trace (succès/échec) en base ou dans un EmailLog/NotificationLog.
|
|
||||||
21. En cas d’échec d’envoi, Messenger applique la stratégie de retry puis bascule en file failed si nécessaire (tableau de bord de supervision).
|
|
||||||
22. Cas “utilisateur existant ajouté à une autre organisation”:
|
|
||||||
23. Si l’email existe déjà, on rattache l’utilisateur à la nouvelle organisation et on publie OrganizationUserInvitedEvent.
|
|
||||||
24. Handler dédié envoie un email d’information (“Vous avez été ajouté à une nouvelle organisation”) et notifie l’admin via Notifier + Mercure.
|
|
||||||
25. Cas d’actions dérivées par enum:
|
|
||||||
26. ActionType::NewUser → déclenche UserInvitedEvent (steps 3–6).
|
|
||||||
27. ActionType::ActiveUser (si activé par un flux admin) → déclenche directement UserActivatedEvent (steps 9–10).
|
|
||||||
28. ActionType::OrganizationUserInvited → flux similaire au point 12 pour la multi‑organisation.
|
|
||||||
29. Autres actions (PasswordReset, UserDeleted, etc.) suivent le même patron: contrôleur → service (match enum) → événement Messenger → handlers (Mailer/Notifier/Mercure) → UI temps réel.
|
|
||||||
|
|
||||||
## Stack technologique
|
|
||||||
- Symfony Messenger: asynchrone, retries, découplage des I/O lents.
|
|
||||||
- Symfony Mailer + Twig: emails d’invitation et d’information.
|
|
||||||
- Symfony Notifier (canal in‑app) + Mercure: notifications persistées + push temps réel vers l’UI admin.
|
|
||||||
- Enum ActionType: routage clair dans l’application, évite la logique string‑based.
|
|
||||||
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
%% Couche 1: Action initiale
|
|
||||||
A[User action event - Admin cree un utilisateur] --> B[HTTP controller API - Symfony]
|
|
||||||
B --> C[Domain service - UserAdministrationService]
|
|
||||||
C -->|Inspecte enum ActionType::NewUser| C1[Create user - status INVITED - liaison organisation - genere lien jeton mot de passe TTL]
|
|
||||||
C1 --> D[Dispatch UserInvitedEvent - userId adminId organizationId - vers Symfony Messenger bus]
|
|
||||||
|
|
||||||
%% Couche 2: Messaging / Infra
|
|
||||||
D --> E[Transport async - AMQP / Redis / Doctrine]
|
|
||||||
E --> RQ[Retry queue]
|
|
||||||
E --> FQ[Failed queue - dead letter]
|
|
||||||
E --> W[Workers Messenger]
|
|
||||||
F[Supervisor / systemd] --> W
|
|
||||||
|
|
||||||
%% Monolog transversal (logs a chaque etape)
|
|
||||||
A --> LOG_GLOBAL[Monolog - log event initial]
|
|
||||||
B --> LOG_GLOBAL
|
|
||||||
C --> LOG_GLOBAL
|
|
||||||
C1 --> LOG_GLOBAL
|
|
||||||
D --> LOG_GLOBAL
|
|
||||||
E --> LOG_GLOBAL
|
|
||||||
RQ --> LOG_GLOBAL
|
|
||||||
FQ --> LOG_GLOBAL
|
|
||||||
W --> LOG_GLOBAL
|
|
||||||
|
|
||||||
%% Handlers pour l'invitation
|
|
||||||
W --> H1[Handler A - Symfony Mailer + Twig]
|
|
||||||
H1 --> H1o[Email d'invitation avec lien setup mot de passe]
|
|
||||||
H1 --> LOG_GLOBAL
|
|
||||||
|
|
||||||
W --> H2[Handler B - Symfony Notifier in-app]
|
|
||||||
H2 --> UI1[Notification UI admin - Email d'invitation envoye]
|
|
||||||
H2 --> LOG_GLOBAL
|
|
||||||
|
|
||||||
W -. optionnel .-> WH1[Webhook HTTP sortant - invitation envoyee]
|
|
||||||
WH1 --> LOG_GLOBAL
|
|
||||||
W -. optionnel .-> SMS1[SMS gateway - SMS invitation]
|
|
||||||
SMS1 --> LOG_GLOBAL
|
|
||||||
W -. optionnel .-> PUSH1[Mobile push service - notification mobile]
|
|
||||||
PUSH1 --> LOG_GLOBAL
|
|
||||||
|
|
||||||
RQ --> METRICS[Metrics et dashboard]
|
|
||||||
FQ --> METRICS
|
|
||||||
LOG_GLOBAL --> METRICS
|
|
||||||
|
|
||||||
%% Flux activation utilisateur
|
|
||||||
subgraph Activation du compte
|
|
||||||
UA[User action event - Invite clique le lien] --> PS[HTTP controller API - PasswordSetupController]
|
|
||||||
PS -->|Verifie signature et TTL| PSOK[Set password - user status ACTIVE]
|
|
||||||
PS --> LOG_GLOBAL
|
|
||||||
PSOK --> LOG_GLOBAL
|
|
||||||
|
|
||||||
PSOK --> D2[Dispatch UserActivatedEvent - userId adminId organizationId - vers Messenger bus]
|
|
||||||
D2 --> E2[Transport async]
|
|
||||||
E2 --> RQ2[Retry queue]
|
|
||||||
E2 --> FQ2[Failed queue]
|
|
||||||
E2 --> W2[Workers Messenger]
|
|
||||||
F --> W2
|
|
||||||
|
|
||||||
D2 --> LOG_GLOBAL
|
|
||||||
E2 --> LOG_GLOBAL
|
|
||||||
RQ2 --> LOG_GLOBAL
|
|
||||||
FQ2 --> LOG_GLOBAL
|
|
||||||
W2 --> LOG_GLOBAL
|
|
||||||
|
|
||||||
W2 --> H3[Handler C - Notifier in-app]
|
|
||||||
H3 --> UI2[Notification UI admin - Compte active]
|
|
||||||
H3 --> LOG_GLOBAL
|
|
||||||
|
|
||||||
W2 -. optionnel .-> WH2[Webhook HTTP sortant - user active]
|
|
||||||
WH2 --> LOG_GLOBAL
|
|
||||||
W2 -. optionnel .-> MAIL2[Mailer ou SMS ou Push - confirmation utilisateur]
|
|
||||||
MAIL2 --> LOG_GLOBAL
|
|
||||||
|
|
||||||
RQ2 --> METRICS
|
|
||||||
FQ2 --> METRICS
|
|
||||||
end
|
|
||||||
|
|
||||||
%% Cas particulier : utilisateur existant ajoute a une nouvelle organisation
|
|
||||||
C -->|Email deja existant| SP1[Rattache a nouvelle organisation]
|
|
||||||
SP1 --> LOG_GLOBAL
|
|
||||||
SP1 --> D3[Dispatch OrganizationUserInvitedEvent]
|
|
||||||
D3 --> E3[Transport async] --> W3[Workers]
|
|
||||||
F --> W3
|
|
||||||
D3 --> LOG_GLOBAL
|
|
||||||
E3 --> LOG_GLOBAL
|
|
||||||
W3 --> LOG_GLOBAL
|
|
||||||
|
|
||||||
W3 --> M3[Mailer - ajoute a une nouvelle organisation]
|
|
||||||
M3 --> LOG_GLOBAL
|
|
||||||
W3 --> N3[Notifier in-app - toast admin Utilisateur ajoute]
|
|
||||||
N3 --> LOG_GLOBAL
|
|
||||||
W3 -. optionnel .-> WH3[Webhook ou SMS ou Mobile]
|
|
||||||
WH3 --> LOG_GLOBAL
|
|
||||||
|
|
||||||
M3 --> METRICS
|
|
||||||
N3 --> METRICS
|
|
||||||
WH3 --> METRICS
|
|
||||||
|
|
||||||
%% Styles
|
|
||||||
classDef infra fill:#e8f0fe,stroke:#5b8def,stroke-width:1px;
|
|
||||||
classDef handler fill:#dcf7e9,stroke:#2ea66a,stroke-width:1px;
|
|
||||||
classDef ui fill:#f0d9ff,stroke:#9c27b0,stroke-width:1px;
|
|
||||||
classDef audit fill:#eeeeee,stroke:#9e9e9e,stroke-width:1px;
|
|
||||||
|
|
||||||
class E,E2,E3,RQ,FQ,RQ2,FQ2,METRICS infra;
|
|
||||||
class W,W2,W3,H1,H2,H3,M3,N3 handler;
|
|
||||||
class H1o,UI1,UI2 ui;
|
|
||||||
class LOG_GLOBAL audit;
|
|
||||||
```
|
|
||||||
33
README.MD
|
|
@ -1,26 +1,23 @@
|
||||||
## Template de base pour les applications de la suite Solutions-easy
|
## Template de base pour les applications de la suite Solutions-easy
|
||||||
|
|
||||||
### Stack technique
|
### Stack technique
|
||||||
- Symfony 7.4
|
- Symfony 7.2
|
||||||
- php 8.2 ou supérieur
|
- php 8.2 ou supérieur
|
||||||
- Stimulus
|
- Stimulus
|
||||||
- Turbo
|
- Turbo
|
||||||
- Bootstrap 5.3
|
- Bootstrap 5.3
|
||||||
|
- Symfony UX toogle password (https://ux.symfony.com/toggle-password)
|
||||||
|
- Les icones sont gérées via symfony UX (https://ux.symfony.com/icons)
|
||||||
|
- Les icones sont prises en prioritées dans la bibliothèque bootstrap
|
||||||
|
- Les icones n'éxistants pas dans cette bibliothèques seront prises en priorité dans fontawesome regular (pour une cohérence visuelle)
|
||||||
|
- Sinon privilégier la bibliothèque ayant le visuel le plus proche
|
||||||
|
|
||||||
|
### Version 0.1 : (17/03/2025)
|
||||||
|
- Contient la logique de login mot de passe avec une entité user (email et password seuelement)
|
||||||
|
- Une base de template twig public est gérée pour les page n'ayant pas besoin de menu
|
||||||
|
- La page de login est designé
|
||||||
|
- Une base de template est gérée pour toutes les pages de l'application aya,t besoin de l'entête et du menu général
|
||||||
|
- Une ébauche de page d'accueil est en cours
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
#### Database
|
|
||||||
```bash
|
|
||||||
php bin/console doctrine:database:create
|
|
||||||
php bin/console doctrine:schema:update --force
|
|
||||||
```
|
|
||||||
#### Roles
|
|
||||||
```bash
|
|
||||||
php bin/console app:create-role USER
|
|
||||||
php bin/console app:create-role ADMIN
|
|
||||||
php bin/console app:create-role "SUPER ADMIN"
|
|
||||||
```
|
|
||||||
#### Choices.js
|
|
||||||
```bash
|
|
||||||
php bin/console importmap:require choices.js
|
|
||||||
php bin/console importmap:require choices.js/public/assets/styles/choices.min.css
|
|
||||||
```
|
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,7 @@ import './bootstrap.js';
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
import './styles/app.css';
|
import './styles/app.css';
|
||||||
import './styles/navbar.css';
|
import './styles/navbar.css';
|
||||||
import './styles/sidebar.css';
|
|
||||||
import './styles/choices.css'
|
|
||||||
import 'choices.js/public/assets/styles/choices.min.css';
|
|
||||||
import 'tabulator-tables/dist/css/tabulator.min.css';
|
|
||||||
import './styles/tabulator.css';
|
|
||||||
import './styles/card.css';
|
|
||||||
import './styles/notifications.css';
|
|
||||||
|
|
||||||
import 'bootstrap';
|
import 'bootstrap';
|
||||||
import './js/template.js';
|
|
||||||
import './js/off_canvas.js';
|
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
|
||||||
import './js/hoverable-collapse.js';
|
|
||||||
import './js/cookies.js';
|
|
||||||
import 'choices.js';
|
|
||||||
import 'quill'
|
|
||||||
import 'tabulator-tables'
|
|
||||||
import './js/global.js'
|
|
||||||
|
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
import {Controller} from '@hotwired/stimulus'
|
|
||||||
import Quill from 'quill'
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
static values = {
|
|
||||||
application: String,
|
|
||||||
organization: String,
|
|
||||||
user: Number,
|
|
||||||
}
|
|
||||||
static targets = ['hidden', 'submitBtn', 'appList']
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
// Map each editor to its toolbar and hidden field
|
|
||||||
if (document.querySelector('#editor-description')) {
|
|
||||||
this.editors = [
|
|
||||||
{
|
|
||||||
editorSelector: '#editor-description',
|
|
||||||
toolbarSelector: '#toolbar-description',
|
|
||||||
hiddenTarget: this.hiddenTargets[0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
editorSelector: '#editor-descriptionSmall',
|
|
||||||
toolbarSelector: '#toolbar-descriptionSmall',
|
|
||||||
hiddenTarget: this.hiddenTargets[1],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
this.editors.forEach(({editorSelector, toolbarSelector, hiddenTarget}) => {
|
|
||||||
const quill = new Quill(editorSelector, {
|
|
||||||
modules: {
|
|
||||||
toolbar: toolbarSelector,
|
|
||||||
},
|
|
||||||
theme: 'snow',
|
|
||||||
placeholder: 'Écrivez votre texte...',
|
|
||||||
})
|
|
||||||
|
|
||||||
quill.on('text-change', () => {
|
|
||||||
hiddenTarget.value = quill.root.innerHTML
|
|
||||||
})
|
|
||||||
|
|
||||||
hiddenTarget.value = quill.root.innerHTML
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if(this.userValue){
|
|
||||||
this.loadApplications();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAuthorizeSubmit(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const originalText = this.submitBtnTarget.textContent;
|
|
||||||
|
|
||||||
if (!confirm(`Vous vous apprêtez à donner l'accès à ${this.organizationValue} pour ${this.applicationValue}. Êtes‑vous sûr(e) ?`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.submitBtnTarget.textContent = 'En cours...';
|
|
||||||
this.submitBtnTarget.disabled = true;
|
|
||||||
|
|
||||||
fetch(event.target.action, {
|
|
||||||
method: 'POST',
|
|
||||||
body: new FormData(event.target)
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
this.submitBtnTarget.textContent = 'Autorisé ✓';
|
|
||||||
this.submitBtnTarget.classList.replace('btn-secondary', 'btn-success');
|
|
||||||
} else {
|
|
||||||
this.submitBtnTarget.textContent = originalText;
|
|
||||||
this.submitBtnTarget.disabled = false;
|
|
||||||
alert('Erreur lors de l\'action');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.submitBtnTarget.textContent = originalText;
|
|
||||||
this.submitBtnTarget.disabled = false;
|
|
||||||
alert('Erreur lors de l\'action');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleRemoveSubmit(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const originalText = this.submitBtnTarget.textContent;
|
|
||||||
|
|
||||||
if (!confirm(`Vous vous apprêtez à retirer l'accès à ${this.applicationValue} pour ${this.organizationValue}. Êtes‑vous sûr(e) ?`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.submitBtnTarget.textContent = 'En cours...';
|
|
||||||
this.submitBtnTarget.disabled = true;
|
|
||||||
|
|
||||||
fetch(event.target.action, {
|
|
||||||
method: 'POST',
|
|
||||||
body: new FormData(event.target)
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
this.submitBtnTarget.textContent = 'Retiré ✓';
|
|
||||||
this.submitBtnTarget.classList.replace('btn-secondary', 'btn-danger');
|
|
||||||
} else {
|
|
||||||
this.submitBtnTarget.textContent = originalText;
|
|
||||||
this.submitBtnTarget.disabled = false;
|
|
||||||
alert('Erreur lors de l\'action');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.submitBtnTarget.textContent = originalText;
|
|
||||||
this.submitBtnTarget.disabled = false;
|
|
||||||
alert('Erreur lors de l\'action');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadApplications() {
|
|
||||||
if (!this.userValue) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Note: Ensure the URL matches your route prefix (e.g. /application/user/123)
|
|
||||||
// Adjust the base path below if your controller route is prefixed!
|
|
||||||
const response = await fetch(`/application/user/${this.userValue}`);
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error("Failed to load apps");
|
|
||||||
|
|
||||||
const apps = await response.json();
|
|
||||||
this.renderApps(apps);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
this.appListTarget.innerHTML = `<span class="text-danger small">Erreur</span>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderApps(apps) {
|
|
||||||
if (apps.length === 0) {
|
|
||||||
// Span 2 columns if empty so the message is centered
|
|
||||||
this.appListTarget.innerHTML = `<span class="text-muted small" style="grid-column: span 2; text-align: center;">Aucune application</span>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const html = apps.map(app => {
|
|
||||||
const url = `https://${app.subDomain}.solutions-easy.com`;
|
|
||||||
|
|
||||||
// Check for logo string vs object
|
|
||||||
const logoSrc = (typeof app.logoMiniUrl === 'string') ? app.logoMiniUrl : '';
|
|
||||||
|
|
||||||
// Render Icon (Image or Fallback)
|
|
||||||
const iconHtml = logoSrc
|
|
||||||
? `<img src="${logoSrc}" style="width:32px; height:32px; object-fit:contain; margin-bottom: 5px;">`
|
|
||||||
: `<i class="bi bi-box-arrow-up-right text-primary" style="font-size: 24px; margin-bottom: 5px;"></i>`;
|
|
||||||
|
|
||||||
// Return a Card-like block
|
|
||||||
return `
|
|
||||||
<a href="${url}" target="_blank"
|
|
||||||
class="d-flex flex-column align-items-center justify-content-center p-3 rounded text-decoration-none text-dark bg-light-hover"
|
|
||||||
style="transition: background 0.2s; height: 100%;">
|
|
||||||
|
|
||||||
${iconHtml}
|
|
||||||
|
|
||||||
<span class="fw-bold text-center text-truncate w-100" style="font-size: 0.85rem;">
|
|
||||||
${app.name}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
this.appListTarget.innerHTML = html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import { Controller } from "@hotwired/stimulus";
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
async fetchAndRenderApplications(targetElement) {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/application/data/all');
|
|
||||||
const apps = await response.json();
|
|
||||||
|
|
||||||
targetElement.innerHTML = apps.map(app => `
|
|
||||||
<div class="col-md-4 mb-3">
|
|
||||||
<div class="form-check border p-2 rounded d-flex align-items-center gap-2">
|
|
||||||
<input class="form-check-input ms-1" type="checkbox" name="applications[]" value="${app.id}" id="app_${app.id}">
|
|
||||||
<label class="form-check-label d-flex align-items-center gap-2" for="app_${app.id}">
|
|
||||||
<img src="${app.logoMiniUrl}" alt="${app.name}" style="height: 20px; width: 20px; object-fit: contain;">
|
|
||||||
${app.name}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
return apps;
|
|
||||||
} catch (error) {
|
|
||||||
targetElement.innerHTML = '<div class="text-danger">Erreur de chargement.</div>';
|
|
||||||
console.error("App load error:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,324 +0,0 @@
|
||||||
import { Controller } from '@hotwired/stimulus';
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
static targets = ['badge', 'list'];
|
|
||||||
static values = {
|
|
||||||
userId: Number,
|
|
||||||
mercureUrl: String,
|
|
||||||
url: String,
|
|
||||||
};
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
this.loadNotifications();
|
|
||||||
this.connectToMercure();
|
|
||||||
this.toastContainer = this.createToastContainer();
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
if (this.eventSource) {
|
|
||||||
this.eventSource.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadNotifications() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/notifications/unread');
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
this.updateBadge(data.unreadCount);
|
|
||||||
this.renderNotifications(data.notifications);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load notifications:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async connectToMercure() {
|
|
||||||
try {
|
|
||||||
// Fetch the JWT token and topic from the server
|
|
||||||
const response = await fetch('/mercure-token');
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
|
|
||||||
// Use server-provided topic if available, otherwise fallback to default per-user topic
|
|
||||||
const topic = data.topic || `${this.urlValue}/notifications/user/${this.userIdValue}`;
|
|
||||||
const url = new URL(this.mercureUrlValue);
|
|
||||||
url.searchParams.append('topic', topic);
|
|
||||||
|
|
||||||
// Add authorization token as URL param if provided (Mercure can accept it this way)
|
|
||||||
if (data.token) {
|
|
||||||
url.searchParams.append('authorization', data.token);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.eventSource = new EventSource(url.toString());
|
|
||||||
} catch (e) {
|
|
||||||
console.error('❌ Failed to create EventSource:', e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.eventSource.onopen = () => {
|
|
||||||
};
|
|
||||||
|
|
||||||
this.eventSource.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const notification = JSON.parse(event.data);
|
|
||||||
this.handleNewNotification(notification);
|
|
||||||
} catch (parseError) {
|
|
||||||
console.error('Failed to parse Mercure message data:', parseError, 'raw data:', event.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.eventSource.onerror = (error) => {
|
|
||||||
try {
|
|
||||||
console.error('EventSource readyState:', this.eventSource.readyState);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Could not read EventSource.readyState:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventSource will automatically try to reconnect.
|
|
||||||
// If closed, log it for debugging.
|
|
||||||
try {
|
|
||||||
if (this.eventSource.readyState === EventSource.CLOSED) {
|
|
||||||
console.log();
|
|
||||||
} else if (this.eventSource.readyState === EventSource.CONNECTING) {
|
|
||||||
console.log();
|
|
||||||
} else if (this.eventSource.readyState === EventSource.OPEN) {
|
|
||||||
console.log();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error while checking EventSource state:', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to connect to Mercure:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNewNotification(notification) {
|
|
||||||
this.showToast(notification);
|
|
||||||
this.loadNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBadge(count) {
|
|
||||||
const badge = this.badgeTarget;
|
|
||||||
if (count > 0) {
|
|
||||||
badge.textContent = count > 99 ? '99+' : count;
|
|
||||||
badge.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
badge.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNotifications(notifications) {
|
|
||||||
const list = this.listTarget;
|
|
||||||
|
|
||||||
if (notifications.length === 0) {
|
|
||||||
list.innerHTML = `
|
|
||||||
<div class="text-center py-4 text-muted">
|
|
||||||
<i class="mx-0 mb-2">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path d="M8.865 2.5a.75.75 0 0 0-1.73 0L6.5 5.5H4.75a.75.75 0 0 0 0 1.5h1.5l-.5 3H3.5a.75.75 0 0 0 0 1.5h1.5l-.635 3.135a.75.75 0 0 0 1.47.28L6.5 11.5h3l-.635 3.135a.75.75 0 0 0 1.47.28L11 11.5h1.75a.75.75 0 0 0 0-1.5h-1.5l.5-3h2.25a.75.75 0 0 0 0-1.5h-2l.635-3.135zM9.5 10l.5-3h-3l-.5 3h3z"/>
|
|
||||||
</svg>
|
|
||||||
</i>
|
|
||||||
<p class="mb-0">Aucune notification</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
list.innerHTML = notifications.map(notif => this.renderNotificationItem(notif)).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNotificationItem(notification) {
|
|
||||||
const iconHtml = this.getIcon(notification.type);
|
|
||||||
const timeAgo = this.getTimeAgo(notification.createdAt);
|
|
||||||
const readClass = notification.isRead ? 'opacity-75' : '';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<a class="dropdown-item preview-item ${readClass}"
|
|
||||||
href="#"
|
|
||||||
data-notification-id="${notification.id}"
|
|
||||||
data-action="click->notification#markAsRead">
|
|
||||||
<div class="preview-thumbnail">
|
|
||||||
<div class="preview-icon ${this.getIconBgClass(notification.type)}">
|
|
||||||
${iconHtml}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="preview-item-content">
|
|
||||||
<h6 class="preview-subject font-weight-normal mb-1">${this.escapeHtml(notification.title)}</h6>
|
|
||||||
<p class="font-weight-light small-text mb-0 text-muted">${this.escapeHtml(notification.message)}</p>
|
|
||||||
<p class="font-weight-light small-text mb-0 text-muted mt-1">
|
|
||||||
<small>${timeAgo}</small>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-link text-danger ms-2"
|
|
||||||
data-action="click->notification#deleteNotification"
|
|
||||||
data-notification-id="${notification.id}"
|
|
||||||
style="padding: 0.25rem;">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
|
||||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getIcon(type) {
|
|
||||||
const icons = {
|
|
||||||
user_joined: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 16 16"><path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10z"/></svg>',
|
|
||||||
user_invited: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 640 512"><path fill="currentColor" d="M224 0a128 128 0 1 1 0 256a128 128 0 1 1 0-256m-45.7 304h91.4c20.6 0 40.4 3.5 58.8 9.9C323 331 320 349.1 320 368c0 59.5 29.5 112.1 74.8 144H29.7C13.3 512 0 498.7 0 482.3C0 383.8 79.8 304 178.3 304M352 368a144 144 0 1 1 288 0a144 144 0 1 1-288 0m144-80c-8.8 0-16 7.2-16 16v64c0 8.8 7.2 16 16 16h48c8.8 0 16-7.2 16-16s-7.2-16-16-16h-32v-48c0-8.8-7.2-16-16-16"/></svg>',
|
|
||||||
user_accepted: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 640 512"><path fill="currentColor" d="M96 128a128 128 0 1 1 256 0a128 128 0 1 1-256 0M0 482.3C0 383.8 79.8 304 178.3 304h91.4c98.5 0 178.3 79.8 178.3 178.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3M625 177L497 305c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L591 143c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg>',
|
|
||||||
user_removed: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 640 512"><path fill="currentColor" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2s-6.3 25.5 4.1 33.7l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L353.3 251.6C407.9 237 448 187.2 448 128C448 57.3 390.7 0 320 0c-69.8 0-126.5 55.8-128 125.2zm225.5 299.2C170.5 309.4 96 387.2 96 482.3c0 16.4 13.3 29.7 29.7 29.7h388.6c3.9 0 7.6-.7 11-2.1z"/></svg>',
|
|
||||||
user_deactivated: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 640 512"><path fill="currentColor" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2s-6.3 25.5 4.1 33.7l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L353.3 251.6C407.9 237 448 187.2 448 128C448 57.3 390.7 0 320 0c-69.8 0-126.5 55.8-128 125.2zm225.5 299.2C170.5 309.4 96 387.2 96 482.3c0 16.4 13.3 29.7 29.7 29.7h388.6c3.9 0 7.6-.7 11-2.1z"/></svg>',
|
|
||||||
org_update: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 16 16"><path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z"/><path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319z"/></svg>',
|
|
||||||
app_access: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 16 16"><path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/></svg>',
|
|
||||||
role_changed: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="mx-0 mb-1" viewBox="0 0 16 16"><path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/></svg>',
|
|
||||||
};
|
|
||||||
return icons[type] || icons.user_joined;
|
|
||||||
}
|
|
||||||
|
|
||||||
getIconBgClass(type) {
|
|
||||||
const classes = {
|
|
||||||
user_joined: 'bg-primary',
|
|
||||||
user_invited: 'bg-info',
|
|
||||||
user_accepted: 'bg-primary',
|
|
||||||
user_removed: 'bg-danger',
|
|
||||||
user_deactivated: 'bg-warning',
|
|
||||||
org_update: 'bg-warning',
|
|
||||||
app_access: 'bg-primary',
|
|
||||||
role_changed: 'bg-info',
|
|
||||||
};
|
|
||||||
return classes[type] || 'bg-primary';
|
|
||||||
}
|
|
||||||
|
|
||||||
getTimeAgo(dateString) {
|
|
||||||
if (!dateString) return '';
|
|
||||||
const date = new Date(dateString);
|
|
||||||
if (isNaN(date)) return '';
|
|
||||||
const now = new Date();
|
|
||||||
const seconds = Math.floor((now - date) / 1000);
|
|
||||||
|
|
||||||
if (seconds < 60) return 'À l\'instant';
|
|
||||||
if (seconds < 3600) {
|
|
||||||
const mins = Math.floor(seconds / 60);
|
|
||||||
return `Il y a ${mins} ${mins > 1 ? 'mins' : 'min'}`;
|
|
||||||
}
|
|
||||||
if (seconds < 86400) {
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
return `Il y a ${hours} ${hours > 1 ? 'h' : 'h'}`;
|
|
||||||
}
|
|
||||||
if (seconds < 604800) {
|
|
||||||
const days = Math.floor(seconds / 86400);
|
|
||||||
return `Il y a ${days} ${days > 1 ? 'j' : 'j'}`;
|
|
||||||
}
|
|
||||||
// For older dates, show a localized date string
|
|
||||||
try {
|
|
||||||
return date.toLocaleDateString('fr-FR', {year: 'numeric', month: 'short', day: 'numeric'});
|
|
||||||
} catch (e) {
|
|
||||||
return date.toLocaleDateString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
escapeHtml(text) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
async markAsRead(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const notificationId = event.currentTarget.dataset.notificationId;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fetch(`/notifications/${notificationId}/read`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loadNotifications();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to mark notification as read:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async markAllAsRead(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fetch('/notifications/mark-all-read', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loadNotifications();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to mark all as read:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteNotification(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const notificationId = event.currentTarget.dataset.notificationId;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fetch(`/notifications/${notificationId}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loadNotifications();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete notification:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
markDropdownAsRead(event) {
|
|
||||||
}
|
|
||||||
|
|
||||||
createToastContainer() {
|
|
||||||
let container = document.getElementById('notification-toast-container');
|
|
||||||
if (!container) {
|
|
||||||
container = document.createElement('div');
|
|
||||||
container.id = 'notification-toast-container';
|
|
||||||
container.className = 'notification-toast-container';
|
|
||||||
document.body.appendChild(container);
|
|
||||||
}
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast(notification) {
|
|
||||||
const toast = document.createElement('div');
|
|
||||||
toast.className = 'notification-toast';
|
|
||||||
toast.innerHTML = `
|
|
||||||
<div class="notification-toast-icon ${this.getIconBgClass(notification.type)}">
|
|
||||||
${this.getIcon(notification.type)}
|
|
||||||
</div>
|
|
||||||
<div class="notification-toast-content">
|
|
||||||
<div class="notification-toast-title">${this.escapeHtml(notification.title)}</div>
|
|
||||||
<div class="notification-toast-message">${this.escapeHtml(notification.message)}</div>
|
|
||||||
</div>
|
|
||||||
<button class="notification-toast-close" onclick="this.parentElement.remove()">×</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.toastContainer.appendChild(toast);
|
|
||||||
|
|
||||||
setTimeout(() => toast.classList.add('show'), 10);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.classList.remove('show');
|
|
||||||
setTimeout(() => toast.remove(), 300);
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
import {Controller} from '@hotwired/stimulus'
|
|
||||||
// Important: include a build with Ajax + pagination (TabulatorFull is simplest)
|
|
||||||
import {TabulatorFull as Tabulator} from 'tabulator-tables';
|
|
||||||
import {capitalizeFirstLetter, eyeIconLink, TABULATOR_FR_LANG} from "../js/global.js";
|
|
||||||
import { Modal } from "bootstrap";
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
static values = {
|
|
||||||
id: Number,
|
|
||||||
activities: Boolean,
|
|
||||||
table: Boolean,
|
|
||||||
sadmin: Boolean,
|
|
||||||
user: Number
|
|
||||||
};
|
|
||||||
|
|
||||||
static targets = ["activityList", "emptyMessage", "modal", "modalTitle", "nameInput", "emailInput", "numberInput", "addressInput"];
|
|
||||||
connect() {
|
|
||||||
if(this.activitiesValue){
|
|
||||||
this.loadActivities();
|
|
||||||
setInterval(() => {
|
|
||||||
this.loadActivities();
|
|
||||||
}, 300000); // Refresh every 5 minutes
|
|
||||||
}
|
|
||||||
if (this.tableValue && this.sadminValue) {
|
|
||||||
this.table();
|
|
||||||
}
|
|
||||||
if (this.hasModalTarget) {
|
|
||||||
this.modal = new Modal(this.modalTarget);
|
|
||||||
this.currentOrgId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
table(){
|
|
||||||
const table = new Tabulator("#tabulator-org", {
|
|
||||||
// Register locales here
|
|
||||||
langs: TABULATOR_FR_LANG,
|
|
||||||
placeholder: "Aucun résultat trouvé pour cette recherche",
|
|
||||||
locale: "fr", //'en' for English, 'fr' for French (en is default, no need to include it)
|
|
||||||
|
|
||||||
ajaxURL: `/organization/data`,
|
|
||||||
ajaxConfig: "GET",
|
|
||||||
pagination: true,
|
|
||||||
paginationMode: "remote",
|
|
||||||
paginationSize: 10,
|
|
||||||
//paginationSizeSelector: [5, 10, 20, 50], // Désactivé pour l'instant car jpp faire de jolie style
|
|
||||||
|
|
||||||
ajaxResponse: (url, params, response) => response,
|
|
||||||
paginationDataSent: { page: "page", size: "size" },
|
|
||||||
paginationDataReceived: { last_page: "last_page",
|
|
||||||
data: "data"},
|
|
||||||
filterMode: "remote",
|
|
||||||
|
|
||||||
ajaxURLGenerator: function(url, config, params) {
|
|
||||||
let queryParams = new URLSearchParams();
|
|
||||||
queryParams.append('page', params.page || 1);
|
|
||||||
queryParams.append('size', params.size || 10);
|
|
||||||
|
|
||||||
// Add filters
|
|
||||||
if (params.filter) {
|
|
||||||
params.filter.forEach(filter => {
|
|
||||||
queryParams.append(`filter[${filter.field}]`, filter.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${url}?${queryParams.toString()}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
ajaxSorting: true,
|
|
||||||
ajaxFiltering: true,
|
|
||||||
rowHeight: 60,
|
|
||||||
layout: "fitColumns",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
title: "Logo",
|
|
||||||
field: "logoUrl",
|
|
||||||
formatter: "image",
|
|
||||||
formatterParams: {
|
|
||||||
height: "50px",
|
|
||||||
width: "50px",
|
|
||||||
urlPrefix: "",
|
|
||||||
urlSuffix: "",
|
|
||||||
},
|
|
||||||
width: 100,
|
|
||||||
},
|
|
||||||
{title: "Nom", field: "name", headerFilter: "input", widthGrow: 2, vertAlign: "middle", headerHozAlign: "left"},
|
|
||||||
{title: "Email", field: "email", headerFilter: "input", widthGrow: 2, vertAlign: "middle", hozAlign: "center"},
|
|
||||||
{
|
|
||||||
title: "Actions",
|
|
||||||
field: "showUrl",
|
|
||||||
hozAlign: "center",
|
|
||||||
width: 100,
|
|
||||||
vertAlign: "middle",
|
|
||||||
headerSort: false,
|
|
||||||
formatter: (cell) => {
|
|
||||||
const url = cell.getValue();
|
|
||||||
if (url) {
|
|
||||||
return eyeIconLink(url);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadActivities() {
|
|
||||||
try {
|
|
||||||
// 1. Fetch the data using the ID from values
|
|
||||||
const response = await fetch(`/actions/organization/${this.idValue}/activities-ajax`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Network response was not ok');
|
|
||||||
}
|
|
||||||
|
|
||||||
const activities = await response.json();
|
|
||||||
|
|
||||||
// 2. Render
|
|
||||||
this.renderActivities(activities);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching activities:', error);
|
|
||||||
this.activityListTarget.innerHTML = `<div class="text-danger">Erreur lors du chargement.</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderActivities(activities) {
|
|
||||||
// Clear the loading spinner
|
|
||||||
this.activityListTarget.innerHTML = '';
|
|
||||||
|
|
||||||
if (activities.length === 0) {
|
|
||||||
// Show empty message
|
|
||||||
this.activityListTarget.innerHTML = this.emptyMessageTarget.innerHTML;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through JSON and build HTML
|
|
||||||
const html = activities.map(activity => {
|
|
||||||
return `
|
|
||||||
<div class="card shadow-sm mb-3 border-0 bg-white rounded-end"
|
|
||||||
style="border-left: 6px solid ${activity.color} !important;">
|
|
||||||
|
|
||||||
<div class="card-header bg-transparent border-0 pb-0 pt-3">
|
|
||||||
<h6 class="text-muted text-uppercase fw-bold mb-0" style="font-size: 0.85rem;">
|
|
||||||
${activity.date}
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-body pt-2 pb-4">
|
|
||||||
<div class="card-text fs-5 lh-sm">
|
|
||||||
<span class="fw-bold text-dark">${activity.userName}</span>
|
|
||||||
<div class="text-secondary mt-1">${activity.actionType}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
//com
|
|
||||||
|
|
||||||
this.activityListTarget.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
openCreateModal() {
|
|
||||||
this.currentOrgId = null;
|
|
||||||
this.modalTitleTarget.textContent = "Créer une organisation";
|
|
||||||
this.resetForm();
|
|
||||||
this.modal.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
async openEditModal(event) {
|
|
||||||
this.currentOrgId = event.currentTarget.dataset.id;
|
|
||||||
this.modalTitleTarget.textContent = "Modifier l'organisation";
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/organization/editModal/${this.currentOrgId}`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Fill targets
|
|
||||||
this.nameInputTarget.value = data.name;
|
|
||||||
this.emailInputTarget.value = data.email;
|
|
||||||
this.numberInputTarget.value = data.number || '';
|
|
||||||
this.addressInputTarget.value = data.address || '';
|
|
||||||
|
|
||||||
this.modal.show();
|
|
||||||
} catch (error) {
|
|
||||||
alert("Erreur lors du chargement des données.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitForm(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const formData = new FormData(event.target);
|
|
||||||
|
|
||||||
const method = this.currentOrgId ? 'PUT' : 'POST';
|
|
||||||
const url = this.currentOrgId ? `/organization/${this.currentOrgId}` : `/organization/`;
|
|
||||||
|
|
||||||
|
|
||||||
if (this.currentOrgId) {
|
|
||||||
formData.append('_method', 'PUT');
|
|
||||||
}
|
|
||||||
formData.set('name', capitalizeFirstLetter(formData.get('name')));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
this.modal.hide();
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
const result = await response.json();
|
|
||||||
alert(result.error || "Une erreur est survenue.");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
alert("Erreur réseau.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetForm() {
|
|
||||||
this.nameInputTarget.value = "";
|
|
||||||
this.emailInputTarget.value = "";
|
|
||||||
this.numberInputTarget.value = "";
|
|
||||||
this.addressInputTarget.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,247 +0,0 @@
|
||||||
import {Controller} from '@hotwired/stimulus';
|
|
||||||
import { Modal } from "bootstrap";
|
|
||||||
import {TabulatorFull as Tabulator} from 'tabulator-tables';
|
|
||||||
import {eyeIconLink, pencilIcon, TABULATOR_FR_LANG, trashIcon} from "../js/global.js";
|
|
||||||
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
static values = {
|
|
||||||
listProject : Boolean,
|
|
||||||
orgId: Number,
|
|
||||||
admin: Boolean
|
|
||||||
}
|
|
||||||
static targets = ["modal", "appList", "nameInput", "formTitle", "timestampSelect", "deletionSelect"];
|
|
||||||
connect(){
|
|
||||||
if(this.listProjectValue){
|
|
||||||
this.table();
|
|
||||||
}
|
|
||||||
this.modal = new Modal(this.modalTarget);
|
|
||||||
}
|
|
||||||
|
|
||||||
table(){
|
|
||||||
const columns = [
|
|
||||||
{title: "<b>ID</b> ", field: "id", visible: false},
|
|
||||||
{title: "<b>Nom du projet</b> ", field: "name", headerFilter: "input", widthGrow: 2, vertAlign: "middle"},
|
|
||||||
{
|
|
||||||
title: "Applications",
|
|
||||||
field: "applications",
|
|
||||||
headerSort: false,
|
|
||||||
hozAlign: "left",
|
|
||||||
formatter: (cell) => {
|
|
||||||
const apps = cell.getValue();
|
|
||||||
if (!apps || apps.length === 0) {
|
|
||||||
return "<span class='text-muted' style='font-size: 0.8rem;'>Aucune</span>";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap everything in a flex container to keep them on one line
|
|
||||||
const content = apps.map(app => `
|
|
||||||
<div class="me-1" title="${app.name}">
|
|
||||||
<img src="${app.logoMiniUrl}"
|
|
||||||
alt="${app.name}"
|
|
||||||
style="height: 35px; width: 35px; object-fit: contain; border-radius: 4px;">
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
return `<div class="d-flex flex-wrap align-items-center">${content}</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
// 2. Add the conditional column if admin value is true
|
|
||||||
if (this.adminValue) {
|
|
||||||
columns.push({
|
|
||||||
title: "<b>Base de données</b>",
|
|
||||||
field: "bddName",
|
|
||||||
hozAlign: "left",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "<b>Actions</b>",
|
|
||||||
field: "id",
|
|
||||||
width: 120,
|
|
||||||
hozAlign: "center",
|
|
||||||
headerSort: false,
|
|
||||||
formatter: (cell) => {
|
|
||||||
const id = cell.getValue();
|
|
||||||
// Return a button that Stimulus can listen to
|
|
||||||
return `<div class="d-flex gap-2 align-content-center">
|
|
||||||
<button class="btn btn-link p-0 border-0" data-action="click->project#openEditModal"
|
|
||||||
data-id="${id}">
|
|
||||||
${pencilIcon()}</button>
|
|
||||||
<button class="btn btn-link p-0 border-0" data-action="click->project#deleteProject"
|
|
||||||
data-id="${id}"> ${trashIcon()} </button>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabulator = new Tabulator("#tabulator-projectListOrganization", {
|
|
||||||
langs: TABULATOR_FR_LANG,
|
|
||||||
locale: "fr",
|
|
||||||
ajaxURL: `/project/organization/data`,
|
|
||||||
ajaxConfig: "GET",
|
|
||||||
pagination: true,
|
|
||||||
paginationMode: "remote",
|
|
||||||
paginationSize: 15,
|
|
||||||
|
|
||||||
ajaxParams: {orgId: this.orgIdValue},
|
|
||||||
ajaxResponse: (url, params, response) => response,
|
|
||||||
paginationDataSent: {page: "page", size: "size"},
|
|
||||||
paginationDataReceived: {last_page: "last_page"},
|
|
||||||
|
|
||||||
ajaxSorting: true,
|
|
||||||
ajaxFiltering: true,
|
|
||||||
filterMode: "remote",
|
|
||||||
|
|
||||||
ajaxURLGenerator: function(url, config, params) {
|
|
||||||
let queryParams = new URLSearchParams();
|
|
||||||
queryParams.append('orgId', params.orgId);
|
|
||||||
queryParams.append('page', params.page || 1);
|
|
||||||
queryParams.append('size', params.size || 15);
|
|
||||||
|
|
||||||
// Add filters
|
|
||||||
if (params.filter) {
|
|
||||||
params.filter.forEach(filter => {
|
|
||||||
queryParams.append(`filter[${filter.field}]`, filter.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${url}?${queryParams.toString()}`;
|
|
||||||
},
|
|
||||||
rowHeight: 60,
|
|
||||||
layout: "fitColumns", // activate French
|
|
||||||
|
|
||||||
columns
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async loadApplications() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/application/data/all');
|
|
||||||
const apps = await response.json();
|
|
||||||
|
|
||||||
this.appListTarget.innerHTML = apps.map(app => `
|
|
||||||
<div class="col-md-4 mb-3">
|
|
||||||
<div class="form-check border p-2 rounded d-flex align-items-center gap-2">
|
|
||||||
<input class="form-check-input ms-1" type="checkbox" name="applications[]" value="${app.id}" id="app_${app.id}">
|
|
||||||
<label class="form-check-label d-flex align-items-center gap-2" for="app_${app.id}">
|
|
||||||
<img src="${app.logoMiniUrl}" alt="${app.name}" style="height: 20px; width: 20px; object-fit: contain;">
|
|
||||||
${app.name}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
} catch (error) {
|
|
||||||
this.appListTarget.innerHTML = '<div class="text-danger">Erreur de chargement.</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitForm(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const form = event.target;
|
|
||||||
const formData = new FormData(form); // This automatically picks up the 'logo' file
|
|
||||||
|
|
||||||
// 1. Validate File Format
|
|
||||||
const logoFile = formData.get('logo');
|
|
||||||
if (logoFile && logoFile.size > 0) {
|
|
||||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg'];
|
|
||||||
if (!allowedTypes.includes(logoFile.type)) {
|
|
||||||
alert("Format invalide. Veuillez utiliser uniquement des fichiers PNG ou JPG.");
|
|
||||||
return; // Stop submission
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Prepare for Multipart sending
|
|
||||||
// Since we are using FormData, we don't need JSON.stringify or 'Content-Type': 'application/json'
|
|
||||||
// We add the extra fields to the formData object
|
|
||||||
formData.append('organizationId', this.orgIdValue);
|
|
||||||
|
|
||||||
const url = this.currentProjectId
|
|
||||||
? `/project/edit/${this.currentProjectId}/ajax`
|
|
||||||
: `/project/new/ajax`;
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
// IMPORTANT: Do NOT set Content-Type header when sending FormData with files
|
|
||||||
// The browser will set 'multipart/form-data' and the boundary automatically
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
this.modal.hide();
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
const result = await response.json();
|
|
||||||
if (response.status === 409) {
|
|
||||||
alert("Un projet avec ce nom existe déjà.");
|
|
||||||
} else {
|
|
||||||
alert(result.error || "Une erreur est survenue.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async openEditModal(event) {
|
|
||||||
const projectId = event.currentTarget.dataset.id;
|
|
||||||
this.currentProjectId = projectId;
|
|
||||||
|
|
||||||
this.modal.show();
|
|
||||||
this.nameInputTarget.disabled = true;
|
|
||||||
this.formTitleTarget.textContent = "Modifier le projet";
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Ensure checkboxes are loaded first
|
|
||||||
await this.loadApplications();
|
|
||||||
|
|
||||||
// 2. Fetch the project data
|
|
||||||
const response = await fetch(`/project/data/${projectId}`);
|
|
||||||
const project = await response.json();
|
|
||||||
|
|
||||||
// 3. Set the name
|
|
||||||
this.nameInputTarget.value = project.name;
|
|
||||||
console.log(project);
|
|
||||||
this.timestampSelectTarget.value = project.timestampPrecision;
|
|
||||||
this.deletionSelectTarget.value = project.deletionAllowed;
|
|
||||||
|
|
||||||
// 4. Check the boxes
|
|
||||||
// We look for all checkboxes inside our appList target
|
|
||||||
const checkboxes = this.appListTarget.querySelectorAll('input[type="checkbox"]');
|
|
||||||
|
|
||||||
checkboxes.forEach(cb => {
|
|
||||||
cb.checked = project.applications.includes(cb.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading project data", error);
|
|
||||||
alert("Erreur lors de la récupération des données du projet.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update your openCreateModal to reset the state
|
|
||||||
openCreateModal() {
|
|
||||||
this.currentProjectId = null;
|
|
||||||
this.modal.show();
|
|
||||||
this.nameInputTarget.disabled = false;
|
|
||||||
this.nameInputTarget.value = "";
|
|
||||||
this.formTitleTarget.textContent = "Nouveau Projet";
|
|
||||||
this.loadApplications();
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteProject(event) {
|
|
||||||
const projectId = event.currentTarget.dataset.id;
|
|
||||||
if (!confirm("Êtes-vous sûr de vouloir supprimer ce projet ?")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/project/delete/${projectId}/ajax`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
}catch (error) {
|
|
||||||
console.error("Error deleting project", error);
|
|
||||||
alert("Erreur lors de la suppression du projet.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path d="M4 2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zm3 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zM4 5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zM7.5 5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zM4.5 8a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5z"/><path d="M2 1a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1zm11 0H3v14h3v-2.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5V15h3z"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 965 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path d="M14.763.075A.5.5 0 0 1 15 .5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5V14h-1v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V10a.5.5 0 0 1 .342-.474L6 7.64V4.5a.5.5 0 0 1 .276-.447l8-4a.5.5 0 0 1 .487.022M6 8.694L1 10.36V15h5zM7 15h2v-1.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5V15h2V1.309l-7 3.5z"/><path d="M2 11h1v1H2zm2 0h1v1H4zm-2 2h1v1H2zm2 0h1v1H4zm4-4h1v1H8zm2 0h1v1h-1zm-2 2h1v1H8zm2 0h1v1h-1zm2-2h1v1h-1zm0 2h1v1h-1zM8 7h1v1H8zm2 0h1v1h-1zm2 0h1v1h-1zM8 5h1v1H8zm2 0h1v1h-1zm2 0h1v1h-1zm0-2h1v1h-1z"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 598 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5M1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 272 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 236 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8L4.646 2.354a.5.5 0 0 1 0-.708"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 236 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path fill-rule="evenodd" d="M5 2a.5.5 0 0 1 .5-.5c.862 0 1.573.287 2.06.566c.174.099.321.198.44.286c.119-.088.266-.187.44-.286A4.17 4.17 0 0 1 10.5 1.5a.5.5 0 0 1 0 1c-.638 0-1.177.213-1.564.434a3.5 3.5 0 0 0-.436.294V7.5H9a.5.5 0 0 1 0 1h-.5v4.272c.1.08.248.187.436.294c.387.221.926.434 1.564.434a.5.5 0 0 1 0 1a4.17 4.17 0 0 1-2.06-.566A5 5 0 0 1 8 13.65a5 5 0 0 1-.44.285a4.17 4.17 0 0 1-2.06.566a.5.5 0 0 1 0-1c.638 0 1.177-.213 1.564-.434c.188-.107.335-.214.436-.294V8.5H7a.5.5 0 0 1 0-1h.5V3.228a3.5 3.5 0 0 0-.436-.294A3.17 3.17 0 0 0 5.5 2.5A.5.5 0 0 1 5 2"/><path d="M10 5h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4v1h4a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-4zM6 5V4H2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4v-1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 827 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1l-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 609 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 464a208 208 0 1 1 0-416a208 208 0 1 1 0 416m0-464a256 256 0 1 0 0 512a256 256 0 1 0 0-512m120.9 294.6c4.5-4.2 7.1-10.1 7.1-16.3c0-12.3-10-22.3-22.3-22.3H304v-96c0-17.7-14.3-32-32-32h-32c-17.7 0-32 14.3-32 32v96h-57.7c-12.3 0-22.3 10-22.3 22.3c0 6.2 2.6 12.1 7.1 16.3l107.1 99.9c3.8 3.5 8.7 5.5 13.8 5.5s10.1-2 13.8-5.5z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 425 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 48a208 208 0 1 1 0 416a208 208 0 1 1 0-416m0 464a256 256 0 1 0 0-512a256 256 0 1 0 0 512M151.2 217.4c-4.6 4.2-7.2 10.1-7.2 16.4c0 12.3 10 22.3 22.3 22.3H208v96c0 17.7 14.3 32 32 32h32c17.7 0 32-14.3 32-32v-96h41.7c12.3 0 22.3-10 22.3-22.3c0-6.2-2.6-12.1-7.2-16.4l-91-84c-3.8-3.5-8.7-5.4-13.9-5.4s-10.1 1.9-13.9 5.4l-91 84z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 428 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M406.5 399.6c-19.1-46.7-65-79.6-118.5-79.6h-64c-53.5 0-99.4 32.9-118.5 79.6C69.9 362.2 48 311.7 48 256c0-114.9 93.1-208 208-208s208 93.1 208 208c0 55.7-21.9 106.2-57.5 143.6m-40.1 32.7c-32 20.1-69.8 31.7-110.4 31.7s-78.4-11.6-110.5-31.7c7.3-36.7 39.7-64.3 78.5-64.3h64c38.8 0 71.2 27.6 78.5 64.3zM256 512a256 256 0 1 0 0-512a256 256 0 1 0 0 512m0-272a40 40 0 1 1 0-80a40 40 0 1 1 0 80m-88-40a88 88 0 1 0 176 0a88 88 0 1 0-176 0"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 528 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256C63 286 89.6 328.5 128 364.3c41.2 38.1 94.8 67.7 160 67.7s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80M95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6M288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80h-2c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2v2c0 44.2 35.8 80 80 80m0-208a128 128 0 1 1 0 256a128 128 0 1 1 0-256"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 787 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" fill-rule="evenodd" d="m243.07 65.728l34.263 14.684v46.42l-41.306-17.703l-107.695 61.54l116.919 66.811L256 243.623v146.285l106.667-60.952v-94.288h42.666v119.048l-10.749 6.143l-149.333 85.333l-10.584 6.048l-10.585-6.048l-149.333-85.333L64 353.716V158.289l10.749-6.142l149.333-85.333l9.224-5.271zm-29.737 324.18V268.383l-106.666-60.952v121.525zm106.666-283.24h55.163l-91.581 91.582l30.17 30.17l91.581-91.582v55.163h42.667v-128h-128z" clip-rule="evenodd"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 548 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M9 17.192V6.808L17.154 12zm1-1.842L15.289 12L10 8.65z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 152 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M10 16h4q.425 0 .713-.288T15 15v-2h-2v1h-2v-4h2v1h2V9q0-.425-.288-.712T14 8h-4q-.425 0-.712.288T9 9v6q0 .425.288.713T10 16m2 6q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 473 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M13 8V4q0-.425.288-.712T14 3h6q.425 0 .713.288T21 4v4q0 .425-.288.713T20 9h-6q-.425 0-.712-.288T13 8M3 12V4q0-.425.288-.712T4 3h6q.425 0 .713.288T11 4v8q0 .425-.288.713T10 13H4q-.425 0-.712-.288T3 12m10 8v-8q0-.425.288-.712T14 11h6q.425 0 .713.288T21 12v8q0 .425-.288.713T20 21h-6q-.425 0-.712-.288T13 20M3 20v-4q0-.425.288-.712T4 15h6q.425 0 .713.288T11 16v4q0 .425-.288.713T10 21H4q-.425 0-.712-.288T3 20m2-9h4V5H5zm10 8h4v-6h-4zm0-12h4V5h-4zM5 19h4v-2H5zm4-2"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 560 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.587 1.413T20 20zm0-2h16V6H4zm0 0V6zm8-5h6q.425 0 .713-.288T19 12V8q0-.425-.288-.712T18 7h-6q-.425 0-.712.288T11 8v4q0 .425.288.713T12 13m1-2V9h4v2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 334 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M11 17h2l.3-1.5q.3-.125.563-.262t.537-.338l1.45.45l1-1.7l-1.15-1q.05-.35.05-.65t-.05-.65l1.15-1l-1-1.7l-1.45.45q-.275-.2-.537-.338T13.3 8.5L13 7h-2l-.3 1.5q-.3.125-.562.263T9.6 9.1l-1.45-.45l-1 1.7l1.15 1q-.05.35-.05.65t.05.65l-1.15 1l1 1.7l1.45-.45q.275.2.538.338t.562.262zm1-3q-.825 0-1.412-.587T10 12t.588-1.412T12 10t1.413.588T14 12t-.587 1.413T12 14m-7 7q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5H5zM5 5v14z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 577 B |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 888 B |
|
Before Width: | Height: | Size: 888 B |
|
|
@ -1,39 +0,0 @@
|
||||||
var application
|
|
||||||
function getApplication(){
|
|
||||||
const body = document.getElementsByTagName('body')[0];
|
|
||||||
application = body.getAttribute('data-application');
|
|
||||||
}
|
|
||||||
|
|
||||||
getApplication();
|
|
||||||
|
|
||||||
// Support pour différents systèmes de navigation
|
|
||||||
if (typeof Turbo !== 'undefined') {
|
|
||||||
document.addEventListener('turbo:load', getApplication);
|
|
||||||
document.addEventListener('turbo:render', getApplication);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Support pour les applications SPA qui utilisent l'historique de navigation
|
|
||||||
window.addEventListener('popstate', getApplication);
|
|
||||||
|
|
||||||
|
|
||||||
window.setCookie = function (cname, cvalue, exdays) {
|
|
||||||
const d = new Date();
|
|
||||||
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
|
|
||||||
let expires = "expires=" + d.toUTCString();
|
|
||||||
document.cookie = application + "-" + cname + "=" + cvalue + ";" + expires + ";path=/;SameSite=Strict";
|
|
||||||
}
|
|
||||||
window.getCookie = function (cname) {
|
|
||||||
let name = application + "-" + cname + "=";
|
|
||||||
let decodedCookie = decodeURIComponent(document.cookie);
|
|
||||||
let ca = decodedCookie.split(';');
|
|
||||||
for (let i = 0; i < ca.length; i++) {
|
|
||||||
let c = ca[i];
|
|
||||||
while (c.charAt(0) === ' ') {
|
|
||||||
c = c.substring(1);
|
|
||||||
}
|
|
||||||
if (c.indexOf(name) === 0) {
|
|
||||||
return c.substring(name.length, c.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
export const TABULATOR_FR_LANG = {
|
|
||||||
fr: {
|
|
||||||
ajax: {loading: "Chargement...", error: "Erreur"},
|
|
||||||
pagination: {
|
|
||||||
page_size: "Taille de page",
|
|
||||||
page_title: "Afficher la page",
|
|
||||||
first: "Premier",
|
|
||||||
first_title: "Première page",
|
|
||||||
last: "Dernier",
|
|
||||||
last_title: "Dernière page",
|
|
||||||
prev: "Précédent",
|
|
||||||
prev_title: "Page précédente",
|
|
||||||
next: "Suivant",
|
|
||||||
next_title: "Page suivante",
|
|
||||||
all: "Tout",
|
|
||||||
counter: {showing: "Affiche", of: "de", rows: "lignes", pages: "pages"},
|
|
||||||
},
|
|
||||||
headerFilters: {default: "Filtrer la colonne...", columns: {}},
|
|
||||||
data: {loading: "Chargement des données...", error: "Erreur de chargement des données"},
|
|
||||||
groups: {item: "élément", items: "éléments"},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function eyeIconLink(url) {
|
|
||||||
return `<a href="${url}" class="p-3 align-middle color-primary" title="Accéder au profil" >
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="35px"
|
|
||||||
height="35px"
|
|
||||||
viewBox="0 0 576 512">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256C63 286 89.6 328.5 128 364.3c41.2 38.1 94.8 67.7 160 67.7s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80M95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6M288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80h-2c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2v2c0 44.2 35.8 80 80 80m0-208a128 128 0 1 1 0 256a128 128 0 1 1 0-256"/></svg>
|
|
||||||
</a>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pencilIcon() {
|
|
||||||
return `
|
|
||||||
<span class="align-middle color-primary">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24">
|
|
||||||
<path fill="currentColor" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5L13.5 4.793L14.793 3.5L12.5 1.207zm1.586 3L10.5 3.207L4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175l-.106.106l-1.528 3.821l3.821-1.528l.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"/></svg>
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function trashIcon() {
|
|
||||||
return `
|
|
||||||
<span class="align-middle color-delete">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24">
|
|
||||||
<path fill="currentColor" d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1l-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"/></svg>
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function trashIconForm(url, organizationId) {
|
|
||||||
return `
|
|
||||||
<button class="btn btn-link p-0 border-0 color-delete align-middle"
|
|
||||||
data-action="click->user#removeAdmin"
|
|
||||||
data-url="${url}"
|
|
||||||
data-org-id="${organizationId}">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24">
|
|
||||||
<path fill="currentColor" d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1l-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"/></svg>
|
|
||||||
|
|
||||||
</button>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function deactivateUserIcon() {
|
|
||||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 640 512">
|
|
||||||
<path fill="currentColor" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2s-6.3 25.5 4.1 33.7l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L353.3 251.6C407.9 237 448 187.2 448 128C448 57.3 390.7 0 320 0c-69.8 0-126.5 55.8-128 125.2zm225.5 299.2C170.5 309.4 96 387.2 96 482.3c0 16.4 13.3 29.7 29.7 29.7h388.6c3.9 0 7.6-.7 11-2.1z"/>
|
|
||||||
</svg>`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function activateUserIcon() {
|
|
||||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 640 512">
|
|
||||||
<path fill="currentColor" d="M96 128a128 128 0 1 1 256 0a128 128 0 1 1-256 0M0 482.3C0 383.8 79.8 304 178.3 304h91.4c98.5 0 178.3 79.8 178.3 178.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3M625 177L497 305c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L591 143c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/>
|
|
||||||
</svg>`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sendEmailIcon(userId, orgId) {
|
|
||||||
return `<a href="#" class="color-primary resend-invitation pt-3" data-id="${userId}" data-org-id="${orgId}" title="Renvoyer l'invitation" >
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24"><path fill="currentColor" d="M18.175 17H15q-.425 0-.712-.288T14 16t.288-.712T15 15h3.175l-.9-.9Q17 13.825 17 13.413t.3-.713q.275-.275.7-.275t.7.275l2.6 2.6q.125.125.2.312t.075.388t-.075.387t-.2.313l-2.6 2.6q-.275.275-.687.288T17.3 19.3q-.275-.275-.275-.7t.275-.7zM4 17q-.825 0-1.412-.587T2 15V5q0-.825.588-1.412T4 3h13q.825 0 1.413.588T19 5v4.075q0 .4-.3.7t-.7.3q-.425 0-.712-.288T17 9.076V6.4L10.4 11L4 6.425V15h7.075q.425 0 .713.288t.287.712t-.287.713t-.713.287zM5.45 5l4.95 3.55L15.5 5zM4 15V5z"/></svg>
|
|
||||||
</a>`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function capitalizeFirstLetter(string) {
|
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
//Open submenu on hover in compact sidebar mode and horizontal menu mode
|
|
||||||
|
|
||||||
|
|
||||||
function initSubMenu(){
|
|
||||||
var sidebar = document.querySelectorAll('.sidebar .nav-item')
|
|
||||||
sidebar.forEach(element => {
|
|
||||||
element.addEventListener('mouseenter', eventMenu)
|
|
||||||
element.addEventListener('mouseleave', eventMenu)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function eventMenu(e){
|
|
||||||
var body = document.querySelector('body');
|
|
||||||
var sidebarIconOnly = body.classList.contains("sidebar-icon-only");
|
|
||||||
var sidebarFixed = body.classList.contains("sidebar-fixed");
|
|
||||||
if (!('ontouchstart' in document.documentElement)) {
|
|
||||||
if (sidebarIconOnly) {
|
|
||||||
var menuItem = this;
|
|
||||||
if (e.type === 'mouseenter') {
|
|
||||||
menuItem.classList.add('hover-open')
|
|
||||||
} else {
|
|
||||||
menuItem.classList.remove('hover-open')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exécuter l'initialisation au chargement de la page
|
|
||||||
document.addEventListener('DOMContentLoaded', initSubMenu);
|
|
||||||
|
|
||||||
// Support pour différents systèmes de navigation
|
|
||||||
if (typeof Turbo !== 'undefined') {
|
|
||||||
document.addEventListener('turbo:load', initSubMenu);
|
|
||||||
document.addEventListener('turbo:render', initSubMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Support pour les applications SPA qui utilisent l'historique de navigation
|
|
||||||
window.addEventListener('popstate', initSubMenu);
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
// Utilisation des fonctions de cookies globales définies dans app.js
|
|
||||||
|
|
||||||
// Fonction d'initialisation du template
|
|
||||||
function initTemplate() {
|
|
||||||
// Appliquer l'état du menu depuis le cookie
|
|
||||||
applyMenuState();
|
|
||||||
|
|
||||||
// Initialiser le bouton de minimisation du menu
|
|
||||||
initMinimizeButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour appliquer l'état du menu depuis le cookie
|
|
||||||
function applyMenuState() {
|
|
||||||
var body = document.querySelector('body');
|
|
||||||
var menuState = getCookie('sidebar_state');
|
|
||||||
|
|
||||||
// Si le cookie existe, appliquer l'état enregistré
|
|
||||||
if (menuState === 'collapsed') {
|
|
||||||
if (!body.classList.contains('sidebar-icon-only')) {
|
|
||||||
body.classList.add('sidebar-icon-only');
|
|
||||||
}
|
|
||||||
} else if (menuState === 'expanded') {
|
|
||||||
if (body.classList.contains('sidebar-icon-only')) {
|
|
||||||
body.classList.remove('sidebar-icon-only');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour initialiser le bouton de minimisation du menu
|
|
||||||
function initMinimizeButton() {
|
|
||||||
// Supprimer l'ancien gestionnaire d'événements s'il existe
|
|
||||||
document.querySelectorAll('[data-toggle="minimize"]').forEach(function(button) {
|
|
||||||
// Créer une copie du bouton pour supprimer tous les écouteurs d'événements
|
|
||||||
var newButton = button.cloneNode(true);
|
|
||||||
button.parentNode.replaceChild(newButton, button);
|
|
||||||
|
|
||||||
// Ajouter le nouvel écouteur d'événements
|
|
||||||
newButton.addEventListener("click", function() {
|
|
||||||
var body = document.querySelector('body');
|
|
||||||
if ((body.classList.contains('sidebar-toggle-display')) || (body.classList.contains('sidebar-absolute'))) {
|
|
||||||
body.classList.toggle('sidebar-hidden');
|
|
||||||
// Enregistrer l'état dans un cookie
|
|
||||||
var newState = body.classList.contains('sidebar-hidden') ? 'collapsed' : 'expanded';
|
|
||||||
setCookie('sidebar_state', newState, 365); // Valable 1 an
|
|
||||||
} else {
|
|
||||||
body.classList.toggle('sidebar-icon-only');
|
|
||||||
// Enregistrer l'état dans un cookie
|
|
||||||
var newState = body.classList.contains('sidebar-icon-only') ? 'collapsed' : 'expanded';
|
|
||||||
setCookie('sidebar_state', newState, 365); // Valable 1 an
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exécuter l'initialisation au chargement de la page
|
|
||||||
document.addEventListener('DOMContentLoaded', initTemplate);
|
|
||||||
|
|
||||||
// Support pour différents systèmes de navigation
|
|
||||||
if (typeof Turbo !== 'undefined') {
|
|
||||||
document.addEventListener('turbo:load', initTemplate);
|
|
||||||
document.addEventListener('turbo:render', initTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Support pour les applications SPA qui utilisent l'historique de navigation
|
|
||||||
window.addEventListener('popstate', initTemplate);
|
|
||||||
|
|
@ -1,19 +1,3 @@
|
||||||
/*variable*/
|
|
||||||
:root{
|
|
||||||
--primary-blue-light : #086572;
|
|
||||||
--primary-blue-dark : #094754;
|
|
||||||
--black-font: #1D1E1C;
|
|
||||||
--delete : #E42E31;
|
|
||||||
--delete-dark : #aa1618;
|
|
||||||
--disable : #A3A3A3;
|
|
||||||
--check : #5cae09;
|
|
||||||
--check-dark: #3a6e05;
|
|
||||||
--secondary : #cc664c;
|
|
||||||
--secondary-dark : #a5543d;
|
|
||||||
--warning : #d2b200;
|
|
||||||
--warning-dark: #c4a600;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -47,141 +31,3 @@ body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-body-wrapper {
|
|
||||||
min-height: calc(100vh - 60px);
|
|
||||||
display: -webkit-flex;
|
|
||||||
display: flex;
|
|
||||||
-webkit-flex-direction: row;
|
|
||||||
flex-direction: row;
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
padding-top: 60px;
|
|
||||||
|
|
||||||
&.full-page-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-panel {
|
|
||||||
transition: width 0.25s ease, margin 0.25s ease;
|
|
||||||
width: calc(100% - 235px);
|
|
||||||
min-height: calc(100vh - 60px);
|
|
||||||
display: -webkit-flex;
|
|
||||||
display: flex;
|
|
||||||
-webkit-flex-direction: column;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper {
|
|
||||||
background: #EEF0FD;
|
|
||||||
width: 100%;
|
|
||||||
-webkit-flex-grow: 1;
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
background: #fff;
|
|
||||||
padding: 10px 2.45rem;
|
|
||||||
transition: all 0.25s ease;
|
|
||||||
-moz-transition: all 0.25s ease;
|
|
||||||
-webkit-transition: all 0.25s ease;
|
|
||||||
-ms-transition: all 0.25s ease;
|
|
||||||
font-size: calc(0.875rem - 0.05rem);
|
|
||||||
font-family: "Nunito", sans-serif;
|
|
||||||
font-weight: 400;
|
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary{
|
|
||||||
background: var(--primary-blue-light);
|
|
||||||
color : #FFFFFF;
|
|
||||||
border: var(--primary-blue-dark);
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
.btn-primary:hover{
|
|
||||||
background: var(--primary-blue-dark);
|
|
||||||
color : #FFFFFF;
|
|
||||||
border: var(--primary-blue-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success{
|
|
||||||
background: var(--check);
|
|
||||||
color : #FFFFFF;
|
|
||||||
border: var(--check-dark);
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
.btn-success:hover{
|
|
||||||
background: var(--check-dark);
|
|
||||||
color : #FFFFFF;
|
|
||||||
border: var(--check);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-warning{
|
|
||||||
background: var(--warning);
|
|
||||||
color : #FFFFFF;
|
|
||||||
border: var(--warning-dark);
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
.btn-warning:hover{
|
|
||||||
background: var(--warning-dark);
|
|
||||||
color : #FFFFFF;
|
|
||||||
border: var(--warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger{
|
|
||||||
background: var(--delete);
|
|
||||||
color : #FFFFFF;
|
|
||||||
border: var(--delete-dark);
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
.btn-danger:hover{
|
|
||||||
background: var(--delete-dark);
|
|
||||||
color : #FFFFFF;
|
|
||||||
border: var(--delete);
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-primary{
|
|
||||||
color: var(--primary-blue-light) !important;
|
|
||||||
}
|
|
||||||
.color-primary-dark{
|
|
||||||
color: var(--primary-blue-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-delete{
|
|
||||||
color: var(--delete) !important;
|
|
||||||
}
|
|
||||||
.color-delete-dark{
|
|
||||||
color: var(--delete-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary{
|
|
||||||
background: var(--secondary);
|
|
||||||
color : #FFFFFF;
|
|
||||||
border: var(--secondary);
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover{
|
|
||||||
background: var(--secondary-dark);
|
|
||||||
color : #FFFFFF;
|
|
||||||
border: var(--secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-warning{
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-secondary{
|
|
||||||
color: var(--secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-primary{
|
|
||||||
background-color: var(--primary-blue-light) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-warning{
|
|
||||||
background-color: var(--secondary) !important;
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
.card.no-header-bg .card-header{
|
|
||||||
background-color: transparent !important;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
.choices {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Input style */
|
|
||||||
.choices__inner {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid var(--primary-blue-light);
|
|
||||||
border-radius: 0.375rem; /* same as Bootstrap `.form-control` */
|
|
||||||
padding: 0.5rem;
|
|
||||||
min-height: 2.5rem;
|
|
||||||
box-shadow: none;
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Placeholder */
|
|
||||||
.choices__placeholder {
|
|
||||||
color: #6c757d; /* Bootstrap muted */
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Selected items (tags) */
|
|
||||||
.choices__list--multiple .choices__item {
|
|
||||||
background-color: var(--primary-blue-light) !important;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
margin: 0.15rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove "x" button */
|
|
||||||
.choices__list--multiple .choices__item .choices__button {
|
|
||||||
border-left: 1px solid rgba(255,255,255,0.3);
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
color: #fff;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
.choices__list--multiple .choices__item .choices__button:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dropdown list */
|
|
||||||
.choices__list--dropdown {
|
|
||||||
border: 1px solid var(--primary-blue-light);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
box-shadow: 0 3px 6px rgba(0,0,0,0.1);
|
|
||||||
margin-top: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dropdown options */
|
|
||||||
.choices__list--dropdown .choices__item {
|
|
||||||
padding: 0.5rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover/active in dropdown */
|
|
||||||
.choices__list--dropdown .choices__item--highlighted {
|
|
||||||
background-color: var(--primary-blue-light);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
@ -69,11 +69,8 @@
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .navbar-menu-wrapper .navbar-toggler:active,
|
.navbar-nav-right{
|
||||||
.navbar .navbar-menu-wrapper .navbar-toggler:focus {
|
flex-direction: row;
|
||||||
outline: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
outline: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .navbar-menu-wrapper .navbar-toggler:not(.navbar-toggler-right) {
|
.navbar .navbar-menu-wrapper .navbar-toggler:not(.navbar-toggler-right) {
|
||||||
|
|
@ -85,24 +82,12 @@
|
||||||
transition: transform 0.3s linear;
|
transition: transform 0.3s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-icon-only .navbar .navbar-menu-wrapper .navbar-toggler:not(.navbar-toggler-right) {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-nav-right{
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .navbar-menu-wrapper .navbar-nav .nav-item.nav-search .input-group{
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .navbar-menu-wrapper .navbar-nav .nav-item.nav-search .input-group .form-control{
|
.navbar .navbar-menu-wrapper .navbar-nav .nav-item.nav-search .input-group .form-control{
|
||||||
margin-left: 0.7rem;
|
margin-left: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#navbar-search-icon > #search{
|
#navbar-search-icon > #search{
|
||||||
vertical-align: baseline;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .navbar-menu-wrapper .navbar-nav.navbar-nav-right {
|
.navbar .navbar-menu-wrapper .navbar-nav.navbar-nav-right {
|
||||||
|
|
@ -259,16 +244,4 @@
|
||||||
#logo_orga{
|
#logo_orga{
|
||||||
width:auto;
|
width:auto;
|
||||||
max-height:40px;
|
max-height:40px;
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .navbar-menu-wrapper .navbar-nav .nav-item.nav-search .input-group .form-control{
|
|
||||||
background: transparent;
|
|
||||||
border: 0;
|
|
||||||
color: #000;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#change-project{
|
|
||||||
padding: 2px 5px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
.notification-toast-container {
|
|
||||||
position: fixed;
|
|
||||||
top: 80px;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 9999;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-toast {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
padding: 16px;
|
|
||||||
min-width: 320px;
|
|
||||||
max-width: 400px;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(400px);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-toast.show {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-toast-icon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-toast-icon svg {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-toast-content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-toast-title {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-toast-message {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-toast-close {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1;
|
|
||||||
color: #999;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-toast-close:hover {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-notif .count-notification {
|
|
||||||
position: absolute;
|
|
||||||
top: 15px;
|
|
||||||
right: -5px;
|
|
||||||
background: var(--primary-blue-light);
|
|
||||||
color: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 3px 7px;
|
|
||||||
font-size: 8px;
|
|
||||||
font-weight: bold;
|
|
||||||
min-width: 5px;
|
|
||||||
height: 10px;
|
|
||||||
line-height:0.5;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item.preview-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item.preview-item:hover {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item.preview-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-thumbnail {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-icon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-icon svg,
|
|
||||||
.preview-icon i {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-item-content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-subject {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-item-content p {
|
|
||||||
font-size: 12px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,290 +0,0 @@
|
||||||
.sidebar {
|
|
||||||
min-height: calc(100vh - 60px);
|
|
||||||
background: #fff;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 0;
|
|
||||||
width: 235px;
|
|
||||||
z-index: 11;
|
|
||||||
transition: width 0.25s ease, background 0.25s ease;
|
|
||||||
-webkit-transition: width 0.25s ease, background 0.25s ease;
|
|
||||||
-moz-transition: width 0.25s ease, background 0.25s ease;
|
|
||||||
-ms-transition: width 0.25s ease, background 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav {
|
|
||||||
overflow: hidden;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav:not(.sub-menu) {
|
|
||||||
padding-top: 1.45rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav .nav-item {
|
|
||||||
-webkit-transition-duration: 0.25s;
|
|
||||||
-moz-transition-duration: 0.25s;
|
|
||||||
-o-transition-duration: 0.25s;
|
|
||||||
transition-duration: 0.25s;
|
|
||||||
transition-property: background;
|
|
||||||
-webkit-transition-property: background;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav .nav-item .collapse {
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav .nav-item.active {
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0px 0px 5px 0px rgba(197, 197, 197, 0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav:not(.sub-menu) > .nav-item {
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-top: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar > .nav:not(.sub-menu) > .nav-item:hover {
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #494949;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav .nav-item .nav-link {
|
|
||||||
display: -webkit-flex;
|
|
||||||
display: flex;
|
|
||||||
-webkit-align-items: center;
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 0.8125rem 1.937rem 0.8125rem 1rem;
|
|
||||||
color: #848484;
|
|
||||||
border-radius: 8px;
|
|
||||||
-webkit-transition-duration: 0.45s;
|
|
||||||
-moz-transition-duration: 0.45s;
|
|
||||||
-o-transition-duration: 0.45s;
|
|
||||||
transition-duration: 0.45s;
|
|
||||||
transition-property: color;
|
|
||||||
-webkit-transition-property: color;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav .nav-item.active > .nav-link {
|
|
||||||
color:lightgrey;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav:not(.sub-menu) > .nav-item > .nav-link {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav .nav-item .nav-link i.menu-icon {
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1;
|
|
||||||
margin-right: 1rem;
|
|
||||||
color: #838383;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav .nav-item .nav-link i.menu-arrow {
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: 0;
|
|
||||||
color: #686868;
|
|
||||||
transform: rotate(0deg);
|
|
||||||
-webkit-transition: transform 0.3s linear;
|
|
||||||
-moz-transition: transform 0.3s linear;
|
|
||||||
-ms-transition: transform 0.3s linear;
|
|
||||||
-o-transition: transform 0.3s linear;
|
|
||||||
transition: transform 0.3s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav .nav-item .nav-link .menu-title {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav .nav-item.active > .nav-link i, .sidebar .nav .nav-item.active > .nav-link .menu-title, .sidebar .nav .nav-item.active > .nav-link .menu-arrow {
|
|
||||||
color: #494949;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav:not(.sub-menu) > .nav-item > .nav-link[aria-expanded=true] {
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
color: #494949;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav:not(.sub-menu) > .nav-item > .nav-link[aria-expanded=true] i.menu-arrow {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.sidebar .nav.sub-menu {
|
|
||||||
margin-bottom: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
list-style: none;
|
|
||||||
padding: 0.25rem 1.5rem 0 2rem;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav.sub-menu .nav-item {
|
|
||||||
position: relative;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: #969696;
|
|
||||||
justify-content: space-between;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav.sub-menu .nav-item svg {
|
|
||||||
position: absolute;
|
|
||||||
color: #b2b2b2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav.sub-menu .nav-item .nav-link {
|
|
||||||
padding: 0.7rem 1rem;
|
|
||||||
position: relative;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1;
|
|
||||||
height: auto;
|
|
||||||
border-top: 0;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav:not(.sub-menu) .nav-link:hover{
|
|
||||||
color: #494949;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
.sidebar-icon-only .sidebar {
|
|
||||||
width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav {
|
|
||||||
overflow: visible;
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .navbar .navbar-brand-wrapper {
|
|
||||||
width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .navbar .navbar-brand-wrapper .brand-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .navbar .navbar-brand-wrapper .brand-logo-mini {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav .nav-item .nav-link {
|
|
||||||
display: block;
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
text-align: center;
|
|
||||||
position: static;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav .nav-item {
|
|
||||||
border-radius: 0px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav .nav-item .collapse {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav:not(.sub-menu) .nav-item.active {
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: 4px 0px 7px 0px rgba(182, 185, 189, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav .nav-item .nav-link i.menu-icon {
|
|
||||||
margin-right: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav .nav-item .nav-link i.menu-arrow {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav.sub-menu {
|
|
||||||
padding: 0 0 0 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav.sub-menu .nav-item .nav-link {
|
|
||||||
text-align: left;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .main-panel {
|
|
||||||
width: calc(100% - 70px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .navbar .navbar-menu-wrapper {
|
|
||||||
width: calc(100% - 70px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav .nav-item .nav-link .menu-title, .sidebar-icon-only .sidebar .nav .nav-item .nav-link .badge, .sidebar-icon-only .sidebar .nav .nav-item .nav-link .menu-sub-title {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav .nav-item .nav-link .menu-title {
|
|
||||||
border-radius: 0 5px 5px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav:not(.sub-menu) > .nav-item:hover {
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0px 0px 5px 0px rgba(197, 197, 197, 0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .nav:not(.sub-menu) > .nav-item:hover,
|
|
||||||
.sidebar-icon-only .nav:not(.sub-menu) > .nav-item:hover .nav-link{
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav .nav-item:hover .nav-link[aria-expanded] .menu-title {
|
|
||||||
border-radius: 0 5px 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav .nav-item.hover-open .nav-link .menu-title {
|
|
||||||
display: -webkit-flex;
|
|
||||||
display: flex;
|
|
||||||
-webkit-align-items: center;
|
|
||||||
align-items: center;
|
|
||||||
background: #ffffff;
|
|
||||||
padding: 0.5rem 1.4rem;
|
|
||||||
left: 70px;
|
|
||||||
position: absolute;
|
|
||||||
text-align: left;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 190px;
|
|
||||||
z-index: 10000;
|
|
||||||
line-height: 1.8;
|
|
||||||
-webkit-box-shadow: 4px 0px 7px 0px rgba(182, 185, 189, 0.25);
|
|
||||||
box-shadow: 4px 0px 7px 0px rgba(182, 185, 189, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav .nav-item.hover-open .nav-link .menu-title:after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon-only .sidebar .nav .nav-item.hover-open .collapse,
|
|
||||||
.sidebar-icon-only .sidebar .nav .nav-item.hover-open .collapsing {
|
|
||||||
display: block;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 0 0 5px 0;
|
|
||||||
position: absolute;
|
|
||||||
left: 70px;
|
|
||||||
width: 190px;
|
|
||||||
-webkit-box-shadow: 4px 4px 7px 0px rgba(182, 185, 189, 0.25);
|
|
||||||
box-shadow: 4px 4px 7px 0px rgba(182, 185, 189, 0.25);
|
|
||||||
}
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
|
|
||||||
/* Remove outer table border */
|
|
||||||
.tabulator {
|
|
||||||
border: none !important;
|
|
||||||
font-size: 18px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove header and row cell borders */
|
|
||||||
.tabulator-header,
|
|
||||||
.tabulator-header .tabulator-col,
|
|
||||||
.tabulator-tableholder,
|
|
||||||
.tabulator-table,
|
|
||||||
.tabulator-row,
|
|
||||||
.tabulator-row .tabulator-cell {
|
|
||||||
border: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove column header bottom border and row separators */
|
|
||||||
.tabulator-header {
|
|
||||||
border-bottom: none !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
/*border-top-left-radius: 25%;*/
|
|
||||||
/*border-top-right-radius: 25%;*/
|
|
||||||
}
|
|
||||||
.tabulator-row {
|
|
||||||
border-bottom: none !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove look on hover/selected without borders */
|
|
||||||
.tabulator-row:hover {
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
.tabulator-row.tabulator-selected {
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabulator-row.tabulator-row-odd {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Rounded border for images in cells */
|
|
||||||
.tabulator-cell img {
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
/* Scope to this table only */
|
|
||||||
.tabulator,
|
|
||||||
.tabulator-header,
|
|
||||||
.tabulator-header .tabulator-header-contents,
|
|
||||||
.tabulator-header .tabulator-col{
|
|
||||||
background: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabulator-footer {border-top: none !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabulator-footer .tabulator-page.active{
|
|
||||||
background-color: var(--primary-blue-light) !important;
|
|
||||||
border: 1px solid var(--primary-blue-light) !important;
|
|
||||||
color: #FFFFFF/* text color */ !important
|
|
||||||
}
|
|
||||||
.tabulator-footer .tabulator-page {
|
|
||||||
background-color: transparent !important;
|
|
||||||
border: 1px solid var(--primary-blue-light) !important;
|
|
||||||
color: var(--black-font)/* text color */ !important;
|
|
||||||
}
|
|
||||||
.tabulator-footer .tabulator-page:hover,
|
|
||||||
.tabulator-footer .tabulator-page.active:hover{
|
|
||||||
background-color: var(--primary-blue-dark) !important;
|
|
||||||
border: 1px solid var(--primary-blue-dark) !important;
|
|
||||||
color: #FFFFFF/* text color */ !important
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabulator-footer select{
|
|
||||||
border: 1px solid var(--primary-blue-light) !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
color: var(--black-font)/* text color */ !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabulator-header input{
|
|
||||||
border: 0;
|
|
||||||
border-radius: 10px;
|
|
||||||
height: 40px;
|
|
||||||
background-color: lightgray !important;
|
|
||||||
padding-left: 15px !important;
|
|
||||||
}
|
|
||||||
.tabulator-header input::placeholder{
|
|
||||||
color: var(--black-font) !important;
|
|
||||||
font-size: 14px !important;
|
|
||||||
opacity: 1 !important; /* Firefox */
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabulator-header input:focus {
|
|
||||||
border:0;
|
|
||||||
}
|
|
||||||
.tabulator .tabulator-header .tabulator-col .tabulator-col-title {
|
|
||||||
font-size: 18px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Select hover Désactivé pour l'instant car jpp faire de jolie style */
|
|
||||||
/*#tabulator-org .tabulator-footer select:hover {*/
|
|
||||||
/* border: 1px solid var(--primary-blue-dark) !important;*/
|
|
||||||
/* background-color: var(--primary-blue-dark) !important;*/
|
|
||||||
/* color: #fff !important;*/
|
|
||||||
/*}*/
|
|
||||||
|
|
||||||
/*.tabulator-footer select:focus {*/
|
|
||||||
/* border: 1px solid var(--primary-blue-dark) !important;*/
|
|
||||||
/* outline: none !important;*/
|
|
||||||
/* background-color: var(--primary-blue-dark) !important;*/
|
|
||||||
/* color: #fff !important;*/
|
|
||||||
/*}*/
|
|
||||||
|
|
@ -8,52 +8,48 @@
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"ext-openssl": "*",
|
"ext-openssl": "*",
|
||||||
"aws/aws-sdk-php-symfony": "^2.8",
|
|
||||||
"doctrine/dbal": "^3",
|
"doctrine/dbal": "^3",
|
||||||
"doctrine/doctrine-bundle": "^2.14",
|
"doctrine/doctrine-bundle": "^2.14",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.4",
|
"doctrine/doctrine-migrations-bundle": "^3.4",
|
||||||
"doctrine/orm": "^3.3",
|
"doctrine/orm": "^3.3",
|
||||||
"firebase/php-jwt": "^7.0.0",
|
"firebase/php-jwt": "^6.11",
|
||||||
"knplabs/knp-time-bundle": "^2.4",
|
|
||||||
"league/oauth2-server-bundle": "^0.11.0",
|
"league/oauth2-server-bundle": "^0.11.0",
|
||||||
"nelmio/cors-bundle": "^2.5",
|
"nelmio/cors-bundle": "^2.5",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
"phpstan/phpdoc-parser": "^2.1",
|
"phpstan/phpdoc-parser": "^2.1",
|
||||||
"symfony/asset": "7.4.*",
|
"symfony/asset": "7.2.*",
|
||||||
"symfony/asset-mapper": "7.4.*",
|
"symfony/asset-mapper": "7.2.*",
|
||||||
"symfony/console": "7.4.*",
|
"symfony/console": "7.2.*",
|
||||||
"symfony/doctrine-messenger": "7.4.*",
|
"symfony/doctrine-messenger": "7.2.*",
|
||||||
"symfony/dotenv": "7.4.*",
|
"symfony/dotenv": "7.2.*",
|
||||||
"symfony/expression-language": "7.4.*",
|
"symfony/expression-language": "7.2.*",
|
||||||
"symfony/flex": "^2",
|
"symfony/flex": "^2",
|
||||||
"symfony/form": "7.4.*",
|
"symfony/form": "7.2.*",
|
||||||
"symfony/framework-bundle": "7.4.*",
|
"symfony/framework-bundle": "7.2.*",
|
||||||
"symfony/http-client": "7.4.*",
|
"symfony/http-client": "7.2.*",
|
||||||
"symfony/intl": "7.4.*",
|
"symfony/intl": "7.2.*",
|
||||||
"symfony/mailer": "7.4.*",
|
"symfony/mailer": "7.2.*",
|
||||||
"symfony/mercure-bundle": "^0.3.9",
|
"symfony/mercure-bundle": "^0.3.9",
|
||||||
"symfony/messenger": "7.4.*",
|
"symfony/mime": "7.2.*",
|
||||||
"symfony/mime": "7.4.*",
|
"symfony/monolog-bundle": "^3.0",
|
||||||
"symfony/monolog-bundle": "^3.10",
|
"symfony/notifier": "7.2.*",
|
||||||
"symfony/notifier": "7.4.*",
|
"symfony/process": "7.2.*",
|
||||||
"symfony/process": "7.4.*",
|
"symfony/property-access": "7.2.*",
|
||||||
"symfony/property-access": "7.4.*",
|
"symfony/property-info": "7.2.*",
|
||||||
"symfony/property-info": "7.4.*",
|
"symfony/runtime": "7.2.*",
|
||||||
"symfony/rate-limiter": "7.4.*",
|
"symfony/security-bundle": "7.2.*",
|
||||||
"symfony/runtime": "7.4.*",
|
"symfony/serializer": "7.2.*",
|
||||||
"symfony/security-bundle": "7.4.*",
|
|
||||||
"symfony/serializer": "7.4.*",
|
|
||||||
"symfony/stimulus-bundle": "^2.24",
|
"symfony/stimulus-bundle": "^2.24",
|
||||||
"symfony/string": "7.4.*",
|
"symfony/string": "7.2.*",
|
||||||
"symfony/translation": "7.4.*",
|
"symfony/translation": "7.2.*",
|
||||||
"symfony/twig-bundle": "7.4.*",
|
"symfony/twig-bundle": "7.2.*",
|
||||||
"symfony/ux-icons": "^2.24",
|
"symfony/ux-icons": "^2.24",
|
||||||
"symfony/ux-toggle-password": "^2.24",
|
"symfony/ux-toggle-password": "^2.24",
|
||||||
"symfony/ux-turbo": "^2.24",
|
"symfony/ux-turbo": "^2.24",
|
||||||
"symfony/validator": "7.4.*",
|
"symfony/validator": "7.2.*",
|
||||||
"symfony/web-link": "7.4.*",
|
"symfony/web-link": "7.2.*",
|
||||||
"symfony/webhook": "7.4.*",
|
"symfony/yaml": "7.2.*",
|
||||||
"symfony/yaml": "7.4.*",
|
"twig/extra-bundle": "^2.12|^3.0",
|
||||||
"twig/twig": "^2.12|^3.0"
|
"twig/twig": "^2.12|^3.0"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
|
@ -104,18 +100,17 @@
|
||||||
"extra": {
|
"extra": {
|
||||||
"symfony": {
|
"symfony": {
|
||||||
"allow-contrib": false,
|
"allow-contrib": false,
|
||||||
"require": "7.4.*"
|
"require": "7.2.*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"dama/doctrine-test-bundle": "^8.3",
|
"phpunit/phpunit": "^9.5",
|
||||||
"phpunit/phpunit": "^11.0",
|
"symfony/browser-kit": "7.2.*",
|
||||||
"symfony/browser-kit": "7.4.*",
|
"symfony/css-selector": "7.2.*",
|
||||||
"symfony/css-selector": "7.4.*",
|
"symfony/debug-bundle": "7.2.*",
|
||||||
"symfony/debug-bundle": "7.4.*",
|
|
||||||
"symfony/maker-bundle": "^1.62",
|
"symfony/maker-bundle": "^1.62",
|
||||||
"symfony/phpunit-bridge": "7.4.*",
|
"symfony/phpunit-bridge": "^7.2",
|
||||||
"symfony/stopwatch": "7.4.*",
|
"symfony/stopwatch": "7.2.*",
|
||||||
"symfony/web-profiler-bundle": "7.4.*"
|
"symfony/web-profiler-bundle": "7.2.*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ return [
|
||||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||||
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
||||||
Symfony\UX\Turbo\TurboBundle::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\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||||
|
|
@ -17,7 +18,4 @@ return [
|
||||||
League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true],
|
League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true],
|
||||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
|
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
|
||||||
Knp\Bundle\TimeBundle\KnpTimeBundle::class => ['all' => true],
|
|
||||||
Aws\Symfony\AwsBundle::class => ['all' => true],
|
|
||||||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||||
|
MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIQ5VGud/OnzsCAggA
|
||||||
|
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECH23Z4CwP/AxBIIEyCUA/sYfp1Vo
|
||||||
|
x+pBPHs8EH5zW+oeCCqihANPGcYXHp3SusSQEA6jyEQz1GHEQF7nt5SxxqYTfe8D
|
||||||
|
ovU2TsspUlycUcZhQQmtM6n2f375G9L2fvIhnOMvD34YvYQ0yhgkToS3h+pORpqb
|
||||||
|
9abJbqkyTHSHOD4utSEaUnFapAQnKPWgFzYSjt4w6pVfX2AUZCXisC3QlcUh4782
|
||||||
|
S62jIfqggMcDU2mfkBBUHE7z1syC5eqYwWEs2P1S1fvSYjpFv7L8LlkZY+6w/qgf
|
||||||
|
IH1ht0A+rs6cQKILXW9yySqRfuumaGJtqQvlql4M7tb7zvDynVgN3/GQxm6oQmFB
|
||||||
|
pFlNWINN7WmKqhRjnJBpKl5L1+s2EWpxBiHXXQLdznqzKyx3tPuLoeVh2S96NEy6
|
||||||
|
f9380eDhNBYKdyUeZP+IBdwtV6U6vbwhtBQ53T9BQ6ykbKpRUm1fI4Y6AcVYW903
|
||||||
|
KCkFR3j6pF4My41AMgcBSJ+GON/PEzIgIIDlYnM+7DVhlentY71Bhf9la7YSaFtx
|
||||||
|
0r8d/CKdgGwPB2+QAEANOo0Z73RmovNRyadvZTcJqEgRC9R+DRbQHNYXN44TEhIU
|
||||||
|
MZaFLTTZHEmWs0f4kf+UUo7RjUi5HdNCjuXErvKgVhtqMofeKc1p6fnNbBCrFYzs
|
||||||
|
Knbwn+u2aLcI7LNLXaSWMOOGrbBw3g5FWObCxZ/FlNABp4yE+YT5YYeCLcemS/8n
|
||||||
|
ZD0l3b2wkLK7dze0DB3TKv+wZoBHtl1s3RYm9UDjB8ejHSzzRMpLK514+Q8Qthx9
|
||||||
|
IexC6hTzIG3wyjst98spb/6VQqDyA4TvMzW+pAA+9JgXyp7auHiU217nx5rSpdTe
|
||||||
|
hWCyRgaBOP48W/ZoBjVxx81/voKixfvBZbYJSciw7K4iNRMjPXNg2BvCZZLZ9Ubd
|
||||||
|
zLVCE15bzdGDZJis/VLawEFHZ1Urku9HfT4FhLxAweEc5VdnbdjEXIXqgf99VaK2
|
||||||
|
V6xXWF9EQ1pVaavR+jzm0ZHs5Ltwnc095+Y9flmvzm+eD00Ftm1IyrfInMl6f8Sv
|
||||||
|
gkxe1lxo6wbTE12CodhkH0woNd+y4K82ZSRzaTg6cUb+YTDKNAfoAnctghPe5+PX
|
||||||
|
3j1ITAQeBFjVXckDrm8jeqF/gbcQYJwFjY0/wtl84gN/Ffs8DSTt1FYwsxgGew43
|
||||||
|
L0KsVt/1Rd4Xb4czBRk7uUpR0U2R7iZ0yIrnGSh657tywpcn2thsJY4X6QjNRWNw
|
||||||
|
yDg5Yrw10tIsLYJLJuxAauGjXbk23lelwnwn8EtMYPI2XtzGSS3b7uGXjTu2pncQ
|
||||||
|
YVFbI/DxrRi3tyF7JLSC1mfNxikMfY6cPp489djDWC0iQVcltrHiHDkmMrloptE7
|
||||||
|
tTUvPfBRNAbonMaXR+pSSQBlDkUMk6uNpk9EYIlLGg2WggOYnikEKsLoX19Mjg5M
|
||||||
|
ZuX7fu2ETHJjvHBetMnQobtqKEGf5RJn+mdKBzia9ZAcjxhKRqYUCK8Qoi3mjvJa
|
||||||
|
x+5idCj2etNJA5idmH50NSXSFbO69Au+LCj1zSikN6h+YgOjqwF0TPQ7Qw4qQQkH
|
||||||
|
4NwYi6S4wCI13LyLVN1XTh9nn99Y/WnORyyegmAfQif1vbcPcfU6mY3VGK5tX2Yl
|
||||||
|
NhLAyGw7m2xDDmIefpRi+w==
|
||||||
|
-----END ENCRYPTED PRIVATE KEY-----
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwLRhFcrWQWeuGhgrFclT
|
||||||
|
c08RTX52kl9XMXatgJi/mfa/o7lbZbhQfNzrMXVZFDTs97YTZXotZVwL1pI7yGtv
|
||||||
|
bjbPNuhs+lvOSmWribIacYx47EqKeYhQ29rOYx7fnr+SvK10QZoFnT58tduQVrER
|
||||||
|
79b+lcbqKMKNeI6zZeVdBrhstuI/PtXnM4kMHThaTz3iLbWkoDyl06VIMFVsvHpc
|
||||||
|
HaPJsBH1I45M9l4gTfshknY3Dz5ESVd41XTH2c/PSa6geNUn5kpslCuxKv3DnmWE
|
||||||
|
h+V87ASCIM7tKE6bc0eFQLKPQo52/TUWfDa8nFbeIrbsQJwq5VhYK21TANNFFL3g
|
||||||
|
GwIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
aws:
|
|
||||||
version: latest
|
|
||||||
region: "%env(AWS_REGION)%"
|
|
||||||
credentials:
|
|
||||||
key: "%env(AWS_KEY)%"
|
|
||||||
secret: "%env(AWS_SECRET)%"
|
|
||||||
S3:
|
|
||||||
region: "%env(AWS_REGION)%"
|
|
||||||
endpoint: "%env(AWS_ENDPOINT)%"
|
|
||||||
use_path_style_endpoint: true
|
|
||||||
signature_version: 'v4'
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
when@test:
|
|
||||||
dama_doctrine_test:
|
|
||||||
enable_static_connection: true
|
|
||||||
enable_static_meta_data_cache: true
|
|
||||||
enable_static_query_cache: true
|
|
||||||
|
|
@ -1,21 +1,17 @@
|
||||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||||
framework:
|
framework:
|
||||||
secret: '%env(APP_SECRET)%'
|
secret: '%env(APP_SECRET)%'
|
||||||
csrf_protection: true
|
trusted_proxies: '%env(TRUSTED_PROXY)%'
|
||||||
http_method_override: true
|
|
||||||
annotations: false
|
|
||||||
handle_all_throwables: true
|
|
||||||
trusted_proxies: '%env(TRUSTED_PROXY)%'
|
|
||||||
|
|
||||||
|
# Note that the session will be started ONLY if you read or write from it.
|
||||||
|
session: true
|
||||||
|
|
||||||
# Note that the session will be started ONLY if you read or write from it.
|
#esi: true
|
||||||
session: true
|
#fragments: true
|
||||||
|
|
||||||
#esi: true
|
|
||||||
#fragments: true
|
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
framework:
|
framework:
|
||||||
test: true
|
test: true
|
||||||
session:
|
session:
|
||||||
storage_factory_id: session.storage.factory.mock_file
|
storage_factory_id: session.storage.factory.mock_file
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ league_oauth2_server:
|
||||||
private_key: '%env(resolve:OAUTH_PRIVATE_KEY)%'
|
private_key: '%env(resolve:OAUTH_PRIVATE_KEY)%'
|
||||||
private_key_passphrase: '%env(resolve:OAUTH_PASSPHRASE)%'
|
private_key_passphrase: '%env(resolve:OAUTH_PASSPHRASE)%'
|
||||||
encryption_key: '%env(resolve:OAUTH_ENCRYPTION_KEY)%'
|
encryption_key: '%env(resolve:OAUTH_ENCRYPTION_KEY)%'
|
||||||
access_token_ttl: PT15M # 15 minutes
|
access_token_ttl: PT3H # 3 hours
|
||||||
refresh_token_ttl: P7D # 7 days
|
refresh_token_ttl: P1M # 1 month
|
||||||
auth_code_ttl: PT30M # 30 minutes
|
auth_code_ttl: PT3H # 10 minutes
|
||||||
require_code_challenge_for_public_clients: false
|
require_code_challenge_for_public_clients: false
|
||||||
resource_server:
|
resource_server:
|
||||||
public_key: '%env(resolve:OAUTH_PUBLIC_KEY)%'
|
public_key: '%env(resolve:OAUTH_PUBLIC_KEY)%'
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,3 @@ mercure:
|
||||||
jwt:
|
jwt:
|
||||||
secret: '%env(MERCURE_JWT_SECRET)%'
|
secret: '%env(MERCURE_JWT_SECRET)%'
|
||||||
publish: '*'
|
publish: '*'
|
||||||
subscribe: '*'
|
|
||||||
|
|
|
||||||
|
|
@ -1,99 +1,27 @@
|
||||||
monolog:
|
monolog:
|
||||||
channels:
|
channels:
|
||||||
- user_management
|
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||||
- authentication
|
|
||||||
- organization_management
|
|
||||||
- access_control
|
|
||||||
- email_notifications
|
|
||||||
- admin_actions
|
|
||||||
- security
|
|
||||||
- php
|
|
||||||
- error
|
|
||||||
- aws_management
|
|
||||||
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
|
||||||
|
|
||||||
when@dev:
|
when@dev:
|
||||||
monolog:
|
monolog:
|
||||||
handlers:
|
handlers:
|
||||||
critical_errors:
|
main:
|
||||||
type: fingers_crossed
|
type: stream
|
||||||
action_level: critical
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
handler: error_nested
|
level: debug
|
||||||
buffer_size: 50
|
channels: ["!event"]
|
||||||
|
# uncomment to get logging in your browser
|
||||||
error_nested:
|
# you may have to allow bigger header sizes in your Web server configuration
|
||||||
type: rotating_file
|
#firephp:
|
||||||
path: "%kernel.logs_dir%/error.log"
|
# type: firephp
|
||||||
level: debug
|
# level: info
|
||||||
max_files: 30
|
#chromephp:
|
||||||
|
# type: chromephp
|
||||||
error:
|
# level: info
|
||||||
type: rotating_file
|
console:
|
||||||
path: "%kernel.logs_dir%/error.log"
|
type: console
|
||||||
level: error # logs error, critical, alert, emergency
|
process_psr_3_messages: false
|
||||||
max_files: 30
|
channels: ["!event", "!doctrine", "!console"]
|
||||||
channels: [ error ]
|
|
||||||
php_errors:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/php_error.log"
|
|
||||||
level: warning # warnings, errors, fatals…
|
|
||||||
max_files: 30
|
|
||||||
channels: [ php ]
|
|
||||||
# User Management
|
|
||||||
user_management:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/user_management.log"
|
|
||||||
level: debug
|
|
||||||
channels: [ user_management ]
|
|
||||||
max_files: 30
|
|
||||||
|
|
||||||
# Authentication
|
|
||||||
authentication:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/authentication.log"
|
|
||||||
level: debug
|
|
||||||
channels: [ authentication ]
|
|
||||||
max_files: 30
|
|
||||||
|
|
||||||
# Organization Management
|
|
||||||
organization_management:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/organization_management.log"
|
|
||||||
level: debug
|
|
||||||
channels: [ organization_management ]
|
|
||||||
max_files: 30
|
|
||||||
|
|
||||||
# Access Control
|
|
||||||
access_control:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/access_control.log"
|
|
||||||
level: debug
|
|
||||||
channels: [ access_control ]
|
|
||||||
max_files: 30
|
|
||||||
|
|
||||||
# Email Notifications
|
|
||||||
email_notifications:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/email_notifications.log"
|
|
||||||
level: debug
|
|
||||||
channels: [ email_notifications ]
|
|
||||||
max_files: 30
|
|
||||||
|
|
||||||
# Admin Actions
|
|
||||||
admin_actions:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/admin_actions.log"
|
|
||||||
level: debug
|
|
||||||
channels: [ admin_actions ]
|
|
||||||
max_files: 30
|
|
||||||
|
|
||||||
# Security
|
|
||||||
security:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/security.log"
|
|
||||||
level: debug
|
|
||||||
channels: [ security ]
|
|
||||||
max_files: 30
|
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
monolog:
|
monolog:
|
||||||
|
|
@ -112,89 +40,23 @@ when@test:
|
||||||
when@prod:
|
when@prod:
|
||||||
monolog:
|
monolog:
|
||||||
handlers:
|
handlers:
|
||||||
critical_error:
|
main:
|
||||||
type: fingers_crossed
|
type: fingers_crossed
|
||||||
action_level: critical
|
action_level: error
|
||||||
handler: error_nested
|
handler: nested
|
||||||
buffer_size: 50
|
excluded_http_codes: [404, 405]
|
||||||
|
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||||
error_nested:
|
nested:
|
||||||
type: rotating_file
|
type: stream
|
||||||
path: "%kernel.logs_dir%/critical.log"
|
path: php://stderr
|
||||||
level: debug
|
level: debug
|
||||||
max_files: 30
|
formatter: monolog.formatter.json
|
||||||
|
console:
|
||||||
error:
|
type: console
|
||||||
type: rotating_file
|
process_psr_3_messages: false
|
||||||
path: "%kernel.logs_dir%/error.log"
|
channels: ["!event", "!doctrine"]
|
||||||
level: error # logs error, critical, alert, emergency
|
deprecation:
|
||||||
max_files: 30
|
type: stream
|
||||||
channels: [ error ]
|
channels: [deprecation]
|
||||||
|
path: php://stderr
|
||||||
php_errors:
|
formatter: monolog.formatter.json
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/php_error.log"
|
|
||||||
level: warning # warnings, errors, fatals…
|
|
||||||
max_files: 30
|
|
||||||
channels: [ php ]
|
|
||||||
# User Management
|
|
||||||
user_management:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/user_management.log"
|
|
||||||
level: info
|
|
||||||
channels: [user_management]
|
|
||||||
max_files: 30
|
|
||||||
#AWS
|
|
||||||
aws_management:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/aws_management.log"
|
|
||||||
level: info
|
|
||||||
channels: [aws_management]
|
|
||||||
max_files: 30
|
|
||||||
# Authentication
|
|
||||||
authentication:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/authentication.log"
|
|
||||||
level: info
|
|
||||||
channels: [authentication]
|
|
||||||
max_files: 30
|
|
||||||
|
|
||||||
# Organization Management
|
|
||||||
organization_management:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/organization_management.log"
|
|
||||||
level: info
|
|
||||||
channels: [organization_management]
|
|
||||||
max_files: 30
|
|
||||||
|
|
||||||
# Access Control
|
|
||||||
access_control:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/access_control.log"
|
|
||||||
level: info
|
|
||||||
channels: [access_control]
|
|
||||||
max_files: 30
|
|
||||||
|
|
||||||
# Email Notifications
|
|
||||||
email_notifications:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/email_notifications.log"
|
|
||||||
level: info
|
|
||||||
channels: [email_notifications]
|
|
||||||
max_files: 30
|
|
||||||
|
|
||||||
# Admin Actions
|
|
||||||
admin_actions:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/admin_actions.log"
|
|
||||||
level: info
|
|
||||||
channels: [admin_actions]
|
|
||||||
max_files: 30
|
|
||||||
|
|
||||||
# Security
|
|
||||||
security:
|
|
||||||
type: rotating_file
|
|
||||||
path: "%kernel.logs_dir%/security.log"
|
|
||||||
level: warning
|
|
||||||
channels: [security]
|
|
||||||
max_files: 30
|
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,13 @@ security:
|
||||||
property: email
|
property: email
|
||||||
|
|
||||||
role_hierarchy:
|
role_hierarchy:
|
||||||
ROLE_ADMIN: ROLE_USER
|
ROLE_ADMIN: ROLE_USER
|
||||||
ROLE_SUPER_ADMIN: [ROLE_ALLOWED_TO_SWITCH, ROLE_ADMIN]
|
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
|
||||||
|
|
||||||
|
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||||
security: false
|
security: false
|
||||||
api_token_validation:
|
|
||||||
pattern: ^/api/validate-token
|
|
||||||
stateless: true
|
|
||||||
oauth2: true
|
|
||||||
oauth_userinfo:
|
oauth_userinfo:
|
||||||
pattern: ^/oauth2/userinfo
|
pattern: ^/oauth2/userinfo
|
||||||
stateless: true
|
stateless: true
|
||||||
|
|
@ -34,35 +29,23 @@ security:
|
||||||
auth_token:
|
auth_token:
|
||||||
pattern: ^/token
|
pattern: ^/token
|
||||||
stateless: true
|
stateless: true
|
||||||
api_m2m:
|
|
||||||
pattern: ^/api/v1/
|
|
||||||
stateless: true
|
|
||||||
oauth2: true
|
|
||||||
api:
|
api:
|
||||||
pattern: ^/oauth/api
|
pattern: ^/oauth/api
|
||||||
security: true
|
security: true
|
||||||
stateless: true
|
stateless: true
|
||||||
oauth2: true
|
oauth2: true
|
||||||
password_setup:
|
|
||||||
pattern: ^/password_setup
|
|
||||||
stateless: true
|
|
||||||
main:
|
main:
|
||||||
user_checker: App\Security\UserChecker
|
|
||||||
lazy: true
|
lazy: true
|
||||||
provider: app_user_provider
|
provider: app_user_provider
|
||||||
login_throttling:
|
|
||||||
max_attempts: 3
|
|
||||||
interval: '1 minute'
|
|
||||||
form_login:
|
form_login:
|
||||||
login_path: app_login
|
login_path: app_login
|
||||||
check_path: app_login
|
check_path: app_login
|
||||||
enable_csrf: true
|
enable_csrf: true
|
||||||
default_target_path: app_index
|
default_target_path: app_index
|
||||||
always_use_default_target_path: false
|
use_referer: true
|
||||||
logout:
|
# logout:
|
||||||
path: app_logout
|
# path: app_logout
|
||||||
enable_csrf: false
|
# target: app_login
|
||||||
target: app_login
|
|
||||||
|
|
||||||
# activate different ways to authenticate
|
# activate different ways to authenticate
|
||||||
# https://symfony.com/doc/current/security.html#the-firewall
|
# https://symfony.com/doc/current/security.html#the-firewall
|
||||||
|
|
@ -74,9 +57,6 @@ security:
|
||||||
# Note: Only the *first* access control that matches will be used
|
# Note: Only the *first* access control that matches will be used
|
||||||
access_control:
|
access_control:
|
||||||
- { path: ^/login, roles: PUBLIC_ACCESS }
|
- { path: ^/login, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/validate-token, roles: PUBLIC_ACCESS }
|
|
||||||
- { path: ^/password_setup, roles: PUBLIC_ACCESS }
|
|
||||||
- { path: ^/password_reset, roles: PUBLIC_ACCESS }
|
|
||||||
- { path: ^/sso_logout, roles: IS_AUTHENTICATED_FULLY }
|
- { path: ^/sso_logout, roles: IS_AUTHENTICATED_FULLY }
|
||||||
- { path: ^/token, roles: PUBLIC_ACCESS }
|
- { path: ^/token, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/oauth2/revoke_tokens, roles: PUBLIC_ACCESS }
|
- { path: ^/oauth2/revoke_tokens, roles: PUBLIC_ACCESS }
|
||||||
|
|
@ -86,6 +66,8 @@ security:
|
||||||
- { path: ^/oauth2/userinfo, roles: IS_AUTHENTICATED_FULLY }
|
- { path: ^/oauth2/userinfo, roles: IS_AUTHENTICATED_FULLY }
|
||||||
- { path: ^/, roles: ROLE_USER }
|
- { path: ^/, roles: ROLE_USER }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
security:
|
security:
|
||||||
password_hashers:
|
password_hashers:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
framework:
|
framework:
|
||||||
default_locale: fr
|
default_locale: en
|
||||||
translator:
|
translator:
|
||||||
default_path: '%kernel.project_dir%/translations'
|
default_path: '%kernel.project_dir%/translations'
|
||||||
fallbacks:
|
fallbacks:
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,6 @@ twig:
|
||||||
file_name_pattern: '*.twig'
|
file_name_pattern: '*.twig'
|
||||||
form_themes: ['bootstrap_5_layout.html.twig']
|
form_themes: ['bootstrap_5_layout.html.twig']
|
||||||
|
|
||||||
globals:
|
|
||||||
application: '%env(APPLICATION)%'
|
|
||||||
domain: '%env(APP_DOMAIN)%'
|
|
||||||
aws_url: '%env(AWS_S3_PORTAL_URL)%'
|
|
||||||
app_url: '%env(APP_URL)%'
|
|
||||||
version: '0.5'
|
|
||||||
|
|
||||||
paths:
|
|
||||||
'%kernel.project_dir%/assets/img': images
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
twig:
|
twig:
|
||||||
strict_variables: true
|
strict_variables: true
|
||||||
|
|
|
||||||
1783
config/reference.php
|
|
@ -4,17 +4,6 @@
|
||||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||||
parameters:
|
parameters:
|
||||||
aws_url: '%env(AWS_ENDPOINT)%'
|
|
||||||
aws_public_url: '%env(AWS_ENDPOINT)%'
|
|
||||||
aws_bucket: '%env(S3_PORTAL_BUCKET)%'
|
|
||||||
app_url: '%env(APP_URL)%'
|
|
||||||
app_domain: '%env(APP_DOMAIN)%'
|
|
||||||
mercure_secret: '%env(MERCURE_JWT_SECRET)%'
|
|
||||||
logos_directory: '%kernel.project_dir%/public/uploads/logos'
|
|
||||||
oauth_sso_identifier: '%env(OAUTH_SSO_IDENTIFIER)%'
|
|
||||||
oauth_sso_identifier_login: '%env(OAUTH_SSO_IDENTIFIER_LOGIN)%'
|
|
||||||
easycheck_url: '%env(EASYCHECK_URL)%'
|
|
||||||
webhook_secret: '%env(WEBHOOK_SECRET)%'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
|
|
@ -30,42 +19,15 @@ services:
|
||||||
- '../src/DependencyInjection/'
|
- '../src/DependencyInjection/'
|
||||||
- '../src/Entity/'
|
- '../src/Entity/'
|
||||||
- '../src/Kernel.php'
|
- '../src/Kernel.php'
|
||||||
App\MessageHandler\NotificationMessageHandler:
|
|
||||||
arguments:
|
|
||||||
$appUrl: '%app_url%'
|
|
||||||
App\Service\SSO\ProjectService:
|
|
||||||
arguments:
|
|
||||||
$appUrl: '%app_url%'
|
|
||||||
$clientIdentifier: '%oauth_sso_identifier%'
|
|
||||||
App\EventSubscriber\:
|
App\EventSubscriber\:
|
||||||
resource: '../src/EventSubscriber/'
|
resource: '../src/EventSubscriber/'
|
||||||
tags: ['kernel.event_subscriber']
|
tags: ['kernel.event_subscriber']
|
||||||
App\EventSubscriber\LoginSubscriber:
|
|
||||||
arguments:
|
|
||||||
$clientIdentifier: '%oauth_sso_identifier_login%'
|
|
||||||
$easycheckUrl: '%env(EASYCHECK_URL)%'
|
|
||||||
App\Service\AwsService:
|
|
||||||
arguments:
|
|
||||||
$awsPublicUrl: '%aws_public_url%'
|
|
||||||
App\Service\OrganizationsService:
|
|
||||||
arguments:
|
|
||||||
$logoDirectory: '%logos_directory%'
|
|
||||||
App\EventSubscriber\ScopeResolveListener:
|
App\EventSubscriber\ScopeResolveListener:
|
||||||
tags:
|
tags:
|
||||||
- { name: kernel.event_listener, event: league.oauth2_server.event.scope_resolve, method: onScopeResolve }
|
- { name: kernel.event_listener, event: league.oauth2_server.event.scope_resolve, method: onScopeResolve }
|
||||||
League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface:
|
League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface:
|
||||||
class: App\Repository\AccessTokenRepository
|
class: App\Repository\AccessTokenRepository
|
||||||
decorates: 'League\Bundle\OAuth2ServerBundle\Repository\AccessTokenRepository'
|
decorates: 'League\Bundle\OAuth2ServerBundle\Repository\AccessTokenRepository'
|
||||||
App\Command\CreateSuperAdminCommand:
|
|
||||||
arguments:
|
# add more service definitions when explicit configuration is needed
|
||||||
$environment: '%kernel.environment%'
|
# please note that last definitions always *replace* previous ones
|
||||||
|
|
||||||
App\EventListener\LogoutSubscriber:
|
|
||||||
arguments:
|
|
||||||
$easycheckUrl: '%env(EASYCHECK_URL)%'
|
|
||||||
tags:
|
|
||||||
- { name: kernel.event_subscriber }
|
|
||||||
App\Webhook\OrganizationNotifier:
|
|
||||||
arguments:
|
|
||||||
$easycheckUrl: '%easycheck_url%'
|
|
||||||
$webhookSecret: '%webhook_secret%'
|
|
||||||
|
|
|
||||||
225
docs/API.md
|
|
@ -1,225 +0,0 @@
|
||||||
# Intro
|
|
||||||
|
|
||||||
The Api made some changes to the current structure of the project. These changes include the following:
|
|
||||||
- **Security** : the security.yaml file has been updated both on the server and the client side
|
|
||||||
- **Controllers**: Controllers need to be updated on the client side
|
|
||||||
- **Services**: Services and token management need updates on the client side
|
|
||||||
- **Health**: I now want to commit a Talon E over a wall IRL
|
|
||||||
- **Entities**: Entities need to be updated on the client side to include a new field (sso_id).
|
|
||||||
- This field will be used a lot, so add it to an entity if you are going to work with the API and the SSO. PLEASE.
|
|
||||||
- **Roles**: A new role was added because we are doing M2M. Only God know how that work now, but it works, so I might start praying from now on.
|
|
||||||
|
|
||||||
|
|
||||||
# Security
|
|
||||||
new firewall was added. Keep the same structure for future firewalls.
|
|
||||||
### Client side
|
|
||||||
```yaml
|
|
||||||
api_project:
|
|
||||||
pattern: ^/api/v1/project #ofc, this is an example, please THINK and change the name
|
|
||||||
stateless: true
|
|
||||||
access_token:
|
|
||||||
token_handler: App\Security\SsoTokenHandler
|
|
||||||
```
|
|
||||||
Same thing, new firewall was added.
|
|
||||||
### Server side
|
|
||||||
```yaml
|
|
||||||
api_token_validation:
|
|
||||||
pattern: ^/api/validate-token #this is NOT an example. DON'T change or it will all go to sh.t
|
|
||||||
stateless: true
|
|
||||||
oauth2: true
|
|
||||||
```
|
|
||||||
```yaml
|
|
||||||
# A rajouter dans l'access_control aussi !!! IMPORTANT !!!
|
|
||||||
- { path: ^/api/validate-token, roles: PUBLIC_ACCESS }
|
|
||||||
```
|
|
||||||
|
|
||||||
# Controllers
|
|
||||||
On the client side, create a new controller for the API. This controller need will work in a REST manner.
|
|
||||||
The route should be as follows:
|
|
||||||
```php
|
|
||||||
#[Route('/api/v1/project', name: 'api_project')] //ofc, this is an example, please THINK and change the name
|
|
||||||
```
|
|
||||||
Keep the same structure for the project tree, create an Api folder in the controller, a V1 folder and then create your controller.
|
|
||||||
Here is a full example of a controller with the create method.
|
|
||||||
On crée une organization si elle n'existe pas
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
#[Route('/api/v2/project', name: 'api_project_')]
|
|
||||||
#[IsGranted('ROLE_API_INTERNAL')]
|
|
||||||
class ProjectApi extends AbstractController{
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private readonly EntityManagerInterface $entityManager)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/create', name: 'create', methods: ['POST'])]
|
|
||||||
public function createProject(Request $request): JSONResponse
|
|
||||||
{
|
|
||||||
$data = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
|
||||||
$projet = new Projet();
|
|
||||||
|
|
||||||
$entity = $this->entityManager->getRepository(Entity::class)->findOneBy([ 'ssoId' => $data['orgId']]);
|
|
||||||
// Si l'entité n'existe pas, on la crée
|
|
||||||
if(!$entity){
|
|
||||||
$this->createEntity($data['orgId'], $data['orgName']);
|
|
||||||
$entity = $this->entityManager->getRepository(Entity::class)->findOneBy([ 'ssoId' => $data['orgId']]);
|
|
||||||
}
|
|
||||||
$precision= $data['timestamp'];
|
|
||||||
$validPrecisions = array_map(fn($case) => $case->value, TimestampPrecision::cases());
|
|
||||||
if (!in_array($precision, $validPrecisions, true)) {
|
|
||||||
return $this->json(['success' => false, 'message' => 'Précision d\'horodatage invalide.'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$timestampPrecision = TimestampPrecision::from($precision);
|
|
||||||
$projet->setTimestampPrecision($timestampPrecision);
|
|
||||||
$projet->setProjet($data['name']);
|
|
||||||
$projet->setEntityId($entity);
|
|
||||||
$projet->setBdd($data['bdd']);
|
|
||||||
$projet->setIsactive($data['isActive']);
|
|
||||||
$projet->setLogo($data['logo']);
|
|
||||||
$projet->setDeletionAllowed($data['deletion']);
|
|
||||||
$projet->setSsoId($data['id']); // c'est l'id du projet dans le portail, pas la bdd local
|
|
||||||
|
|
||||||
$this->entityManager->persist($projet);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
return new JsonResponse(['message' => 'Project created successfully', 'project_id' => $projet->getId()], 201);
|
|
||||||
}catch ( \Exception $e){
|
|
||||||
return new JsonResponse(['error' => 'Failed to create project: ' . $e->getMessage()], 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Services
|
|
||||||
So, now we are getting into the thick of it. We are just COOK 🍗 ( a lot of bugs can come from here ).
|
|
||||||
We implement a new pretty service called SsoTokenHandler. This is used to get the token received from the portal request, and validate it.
|
|
||||||
It is validaded by doing a call back to the SSO and asking if the token is valid. ( we create a new token for the SSO so it handles M2M)
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
// src/Security/SsoTokenHandler.php
|
|
||||||
namespace App\Security;
|
|
||||||
|
|
||||||
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
|
|
||||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
|
||||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
|
||||||
use Symfony\Component\Security\Core\User\InMemoryUser;
|
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
||||||
|
|
||||||
class SsoTokenHandler implements AccessTokenHandlerInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private HttpClientInterface $httpClient,
|
|
||||||
private string $ssoUrl
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function getUserBadgeFrom(string $accessToken): UserBadge
|
|
||||||
{
|
|
||||||
// 1. Call the SSO to validate the token
|
|
||||||
// Note: You need an endpoint on your SSO that returns 200 for M2M tokens
|
|
||||||
try {
|
|
||||||
$response = $this->httpClient->request('GET', $this->ssoUrl . '/api/validate-token', [
|
|
||||||
'headers' => ['Authorization' => 'Bearer ' . $accessToken]
|
|
||||||
]);
|
|
||||||
|
|
||||||
// If the SSO redirects, HttpClient might follow it to the login page.
|
|
||||||
// Let's see the first response code.
|
|
||||||
if ($response->getStatusCode() !== 200) {
|
|
||||||
// Log the content to see the HTML "Login" page that is causing the JSON error
|
|
||||||
// dump($response->getContent(false));
|
|
||||||
throw new BadCredentialsException();
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
// This will show you if you are hitting a 302 Redirect
|
|
||||||
throw new BadCredentialsException('SSO returned invalid response: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (200 !== $response->getStatusCode()) {
|
|
||||||
throw new BadCredentialsException('Invalid SSO Token');
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $response->toArray();
|
|
||||||
|
|
||||||
// 2. Identify if it's a User or a Machine
|
|
||||||
$identifier = $data['email'] ?? 'SYSTEM_SSO_SERVER';
|
|
||||||
|
|
||||||
// 3. Return the badge with a "loader" closure
|
|
||||||
return new UserBadge($identifier, function($userIdentifier) use ($data) {
|
|
||||||
// If it's the SSO server calling, give it a specific role
|
|
||||||
if ($userIdentifier === 'SYSTEM_SSO_SERVER') {
|
|
||||||
return new InMemoryUser($userIdentifier, null, ['ROLE_API_INTERNAL']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, let the normal user provider handle it (for standard users)
|
|
||||||
// You might need to inject your actual UserProvider here if needed
|
|
||||||
return new InMemoryUser($userIdentifier, null, ['ROLE_USER']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Important note 1
|
|
||||||
we need to add the portal url to the .env and declare it as a parameter in services.yaml
|
|
||||||
```dotenv
|
|
||||||
SSO_URL='http://portail.solutions-easy.moi'
|
|
||||||
```
|
|
||||||
```yaml
|
|
||||||
parameters:
|
|
||||||
sso_url: '%env(SSO_URL)%'
|
|
||||||
|
|
||||||
App\Security\SsoTokenHandler:
|
|
||||||
arguments:
|
|
||||||
$ssoUrl: '%sso_url%'
|
|
||||||
```
|
|
||||||
# Server side
|
|
||||||
On the server side, we need to create a new client, which will be himself, same as earlier, create it, and boom we are good.
|
|
||||||
The validate route is already created, so dw abt it.
|
|
||||||
|
|
||||||
```cmd
|
|
||||||
php bin/console league:oauth2-server:create-client sso_internal_service --grant-type "client_credentials"
|
|
||||||
```
|
|
||||||
|
|
||||||
now, copy the identifier, and paste it in the .env file
|
|
||||||
please note that we have 2 client for the application because one is used for m2m and the other is used for the user, so implement both, the one ending with _LOGIN is the one for the user
|
|
||||||
```dotenv
|
|
||||||
OAUTH_SSO_IDENTIFIER='sso-own-identifier'
|
|
||||||
```
|
|
||||||
and we are smart so what do we do? we add it to the services.yaml
|
|
||||||
```yaml
|
|
||||||
parameters:
|
|
||||||
oauth_sso_identifier: '%env(OAUTH_SSO_IDENTIFIER)%'
|
|
||||||
|
|
||||||
App\Service\SSO\ProjectService:
|
|
||||||
arguments:
|
|
||||||
$appUrl: '%app_url%'
|
|
||||||
$clientIdentifier: '%oauth_sso_identifier%'
|
|
||||||
```
|
|
||||||
|
|
||||||
We should be good now ( I hope ). Open the portal, try your call and check if it works, if it doesn't, check the logs and debug, you are a dev for a reason, so use your brain and debug.
|
|
||||||
If it still doesn't work, start praying, because I have no idea what to do anymore, but it works on my side, so it should work on yours, if not, well, I don't know what to say. Good luck.
|
|
||||||
Jokes aside, bugs often come from security problem, if the client returns a 401 error, it can be for multiple reasons and not necessarily because of the token but maybe because of the token validation.
|
|
||||||
Another commun bug is mismatching of the data you send, so double check. GLHF ( you won't have fun, but good luck anyway )
|
|
||||||
|
|
||||||
.
|
|
||||||
|
|
||||||
.
|
|
||||||
|
|
||||||
.
|
|
||||||
|
|
||||||
.
|
|
||||||
|
|
||||||
.
|
|
||||||
|
|
||||||
.
|
|
||||||
|
|
||||||
.
|
|
||||||
|
|
||||||
.
|
|
||||||
|
|
||||||
.
|
|
||||||
|
|
||||||
.
|
|
||||||
|
|
||||||
.
|
|
||||||
If you have a problem and that you copy pasted everything, check client controller, I've put the route as /api/v2 just cause I felt like it 🤷 ENJOY
|
|
||||||
|
|
@ -1,365 +0,0 @@
|
||||||
# Client setup
|
|
||||||
## Add needed dependencies
|
|
||||||
```bash
|
|
||||||
composer require nelmio/cors-bundle
|
|
||||||
composer require knpuniversity/oauth2-client-bundle
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configure the bundle
|
|
||||||
### nelmio/cors-bundle
|
|
||||||
```yaml
|
|
||||||
# config/packages/nelmio_cors.yaml
|
|
||||||
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:
|
|
||||||
'^/token$':
|
|
||||||
origin_regex: true
|
|
||||||
allow_origin: ['*']
|
|
||||||
allow_headers: ['Content-Type', 'Authorization']
|
|
||||||
allow_methods: ['POST', 'OPTIONS']
|
|
||||||
allow_credentials: true
|
|
||||||
max_age: 3600
|
|
||||||
'^/authorize$':
|
|
||||||
origin_regex: true
|
|
||||||
allow_origin: ['*']
|
|
||||||
allow_headers: ['Content-Type', 'Authorization']
|
|
||||||
allow_methods: ['GET', 'POST', 'OPTIONS']
|
|
||||||
allow_credentials: true
|
|
||||||
max_age: 3600
|
|
||||||
```
|
|
||||||
### knpuniversity/oauth2-client-bundle
|
|
||||||
```yaml
|
|
||||||
# config/packages/knpu_oauth2_client.yaml
|
|
||||||
knpu_oauth2_client:
|
|
||||||
clients:
|
|
||||||
# configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration
|
|
||||||
sudalys:
|
|
||||||
type: generic
|
|
||||||
provider_class: Sudalys\OAuth2\Client\Provider\Sudalys
|
|
||||||
client_id: '%env(OAUTH2_CLIENT_ID)%'
|
|
||||||
client_secret: '%env(OAUTH2_CLIENT_SECRET)%'
|
|
||||||
redirect_route: uri # The route to redirect to after authentication (must match the one in the server DB uri DB)
|
|
||||||
provider_options: {
|
|
||||||
domain: <link to domain>
|
|
||||||
}
|
|
||||||
use_state: false
|
|
||||||
```
|
|
||||||
|
|
||||||
### .env
|
|
||||||
```dotenv
|
|
||||||
# .env
|
|
||||||
# CORS
|
|
||||||
CORS_ALLOW_ORIGIN=http://*.your domain/*'
|
|
||||||
# OAUTH2
|
|
||||||
OAUTH2_CLIENT_ID=<client_id>
|
|
||||||
OAUTH2_CLIENT_SECRET=<client_secret>
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy and paste the client library then modify the conposer.json autoloard directive to include the new library
|
|
||||||
```json
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"App\\": "src/",
|
|
||||||
"Sudalys\\OAuth2\\Client\\": "libs/sudalys/oauth2-client/src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
/** @var string */
|
|
||||||
public $domain = 'link to SSO portal ';
|
|
||||||
```
|
|
||||||
Copy and paste the SSOAuthenticator class modify the target url to match the route in server DB and redirect route
|
|
||||||
### SsoAuthenticator.php
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Security;
|
|
||||||
|
|
||||||
use App\Entity\User;
|
|
||||||
|
|
||||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
||||||
use Symfony\Component\Routing\RouterInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
|
||||||
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|
||||||
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
|
|
||||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
|
||||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
|
||||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
|
||||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
|
||||||
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class SudalysSSoAuthenticator
|
|
||||||
*/
|
|
||||||
class SsoAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface
|
|
||||||
{
|
|
||||||
private $clientRegistry;
|
|
||||||
private $em;
|
|
||||||
private $router;
|
|
||||||
private $urlGenerator;
|
|
||||||
|
|
||||||
use TargetPathTrait;
|
|
||||||
|
|
||||||
public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $em, RouterInterface $router, UrlGeneratorInterface $urlGenerator)
|
|
||||||
{
|
|
||||||
$this->clientRegistry = $clientRegistry;
|
|
||||||
$this->em = $em;
|
|
||||||
$this->router = $router;
|
|
||||||
$this->urlGenerator = $urlGenerator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function start(Request $request, AuthenticationException $authException = null): Response
|
|
||||||
{
|
|
||||||
// Use the KnpU client to generate the correct authorization URL,
|
|
||||||
// including state / redirect_uri / scope / pkce as configured.
|
|
||||||
$client = $this->getSudalysClient();
|
|
||||||
|
|
||||||
// Option A: let the client use the configured redirect uri and default scopes:
|
|
||||||
return $client->redirect();
|
|
||||||
|
|
||||||
// Option B (explicit): specify scopes and an explicit redirect_uri (absolute URL)
|
|
||||||
// $redirectUri = $this->urlGenerator->generate('sudalys_check', [], UrlGeneratorInterface::ABSOLUTE_URL);
|
|
||||||
// return $client->redirect(['openid', 'profile'], ['redirect_uri' => $redirectUri]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function supports(Request $request): ?bool
|
|
||||||
{
|
|
||||||
// If your OAuth redirect route is named 'sudalys_check', check by route:
|
|
||||||
if ($request->attributes->get('_route') === 'sudalys_check') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallback: also support requests containing the authorization code
|
|
||||||
return (bool) $request->query->get('code');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function authenticate(Request $request): Passport
|
|
||||||
{
|
|
||||||
$client = $this->getSudalysClient();
|
|
||||||
$accessToken = $this->fetchAccessToken($client);
|
|
||||||
$session = $request->getSession();
|
|
||||||
$session->set('access_token', $accessToken->getToken());
|
|
||||||
|
|
||||||
// Stocker également le refresh token s'il est disponible
|
|
||||||
if ($accessToken->getRefreshToken()) {
|
|
||||||
$session->set('refresh_token', $accessToken->getRefreshToken());
|
|
||||||
}
|
|
||||||
return new SelfValidatingPassport(
|
|
||||||
new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) {
|
|
||||||
//show in log the access token
|
|
||||||
$sudalysSsoUser = $client->fetchUserFromToken($accessToken);
|
|
||||||
|
|
||||||
$ssoId = $sudalysSsoUser->getId();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* On regarde si le token est valide
|
|
||||||
*/
|
|
||||||
if($accessToken->getExpires() > time()) {
|
|
||||||
// Token valide, on regarde si l'utilisateur existe en bdd locale
|
|
||||||
/** @var User $userInDatabase */
|
|
||||||
$user = $this->em->getRepository(User::class)->findOneBy(['ssoId' => $ssoId]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* on cree l'utilisateur s'il n'existe pas
|
|
||||||
**/
|
|
||||||
if (!$user) {
|
|
||||||
$user = new User();
|
|
||||||
$user->setEmail($ssoData->getEmail());
|
|
||||||
$user->setPrenom($ssoData->getName());
|
|
||||||
$user->setNom($ssoData->getSurname());
|
|
||||||
$user->setSsoId($ssoData->getId());
|
|
||||||
$this->em->persist($user);
|
|
||||||
}else{
|
|
||||||
// On met a jour l'utilisateur
|
|
||||||
$user->setEmail($ssoData->getEmail());
|
|
||||||
$user->setPrenom($ssoData->getName());
|
|
||||||
$user->setNom($ssoData->getSurname());
|
|
||||||
$this->em->persist($user);
|
|
||||||
}
|
|
||||||
|
|
||||||
//handle UOs links
|
|
||||||
$ssoArray = $ssoData->toArray();
|
|
||||||
$uoData = $ssoArray['uos'] ?? [];
|
|
||||||
foreach ($uoData as $uo) {
|
|
||||||
$ssoOrgId = $uo['id'];
|
|
||||||
|
|
||||||
$entity = $this->em->getRepository(Entity::class)->findOneBy(['ssoId' => $ssoOrgId]);
|
|
||||||
if (!$entity) {
|
|
||||||
$entity = new Entity();
|
|
||||||
$entity->setSsoId($ssoOrgId);
|
|
||||||
$entity->setNom($uo['name']);
|
|
||||||
$this->em->persist($entity);
|
|
||||||
}
|
|
||||||
$role = $this->em->getRepository(Roles::class)->findOneBy(['name' => $uo['role']]);
|
|
||||||
|
|
||||||
// Check if the user-organization link already exists
|
|
||||||
$existingLink = $this->em->getRepository(UsersOrganizations::class)->findOneBy([
|
|
||||||
'users' => $user,
|
|
||||||
'organizations' => $entity
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!$existingLink) {
|
|
||||||
// Create a new link if it doesn't exist
|
|
||||||
$newLink = new UsersOrganizations();
|
|
||||||
$newLink->setUsers($user);
|
|
||||||
$newLink->setOrganizations($entity);
|
|
||||||
$newLink->setRole($role);
|
|
||||||
$this->em->persist($newLink);
|
|
||||||
} else {
|
|
||||||
// Update the role if the link already exists
|
|
||||||
$existingLink->setRole($role);
|
|
||||||
$existingLink->setModifiedAt(new \DateTimeImmutable());
|
|
||||||
$this->em->persist($existingLink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$this->em->flush();
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
|
||||||
{
|
|
||||||
// change "app_homepage" to some route in your app
|
|
||||||
$targetUrl = $this->router->generate('app_index');
|
|
||||||
return new RedirectResponse($targetUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
|
||||||
{
|
|
||||||
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
|
|
||||||
return new Response($message, Response::HTTP_FORBIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private function getSudalysClient()
|
|
||||||
{
|
|
||||||
return $this->clientRegistry->getClient('sudalys');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
```php
|
|
||||||
namespace App\Security\SsoAuthenticator;
|
|
||||||
|
|
||||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
|
||||||
{
|
|
||||||
// change "app_homepage" to some route in your app
|
|
||||||
$targetUrl = $this->router->generate('your redirect route');
|
|
||||||
return new RedirectResponse($targetUrl);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security.yaml
|
|
||||||
```yaml
|
|
||||||
app_user_provider:
|
|
||||||
entity:
|
|
||||||
class: App\Entity\User
|
|
||||||
property: email
|
|
||||||
|
|
||||||
firewalls:
|
|
||||||
main:
|
|
||||||
lazy: true
|
|
||||||
provider: app_user_provider
|
|
||||||
custom_authenticators:
|
|
||||||
- App\Security\SsoAuthenticator
|
|
||||||
entry_point: App\Security\SsoAuthenticator
|
|
||||||
logout:
|
|
||||||
path: app_logout
|
|
||||||
target: app_after_logout
|
|
||||||
invalidate_session: true
|
|
||||||
delete_cookies: ['PHPSESSID']
|
|
||||||
|
|
||||||
access_control:
|
|
||||||
- { path: ^/sso/login, roles: PUBLIC_ACCESS }
|
|
||||||
- { path: ^/sso/check, roles: PUBLIC_ACCESS }
|
|
||||||
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
|
|
||||||
```
|
|
||||||
### Setup oauth controller
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Controller;
|
|
||||||
|
|
||||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Psr\Log\LogLevel;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
||||||
|
|
||||||
class SsoController extends AbstractController
|
|
||||||
{
|
|
||||||
|
|
||||||
#[Route('/sso/login', name: 'sudalys_sso_login')]
|
|
||||||
public function login(ClientRegistry $clientRegistry): RedirectResponse
|
|
||||||
{
|
|
||||||
return $clientRegistry->getClient('sudalys')->redirect();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/sso/check', name: 'sudalys_sso_check')]
|
|
||||||
public function connectCheckAction(Request $request)
|
|
||||||
{
|
|
||||||
return $this->redirectToRoute('app_index');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[Route('/logout', name: 'app_logout')]
|
|
||||||
public function logout(): void
|
|
||||||
{
|
|
||||||
throw new \Exception('This should never be reached!');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/logout-redirect', name: 'app_after_logout')]
|
|
||||||
public function afterLogout(): RedirectResponse
|
|
||||||
{
|
|
||||||
// SSO logout URL — adjust if necessary
|
|
||||||
$ssoLogout = 'http://portail.solutions-easy.moi/sso_logout';
|
|
||||||
|
|
||||||
return new RedirectResponse($ssoLogout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
# Server setup
|
|
||||||
## Create OAuth2 client
|
|
||||||
```cmd
|
|
||||||
php bin/console league:oauth2-server:create-client <name> --redirect-uri="http://your-client-domain/sso/check" --scope="openid" --scope="profile" --scope="email" --grant-type=authorization_code
|
|
||||||
```
|
|
||||||
If there is a scope or grand error, delete the client do the following first
|
|
||||||
```cmd
|
|
||||||
php bin/console league:oauth2-server:delete-client <identifier>
|
|
||||||
```
|
|
||||||
Identifier can be found in the database oauth2_client table
|
|
||||||
To recreate the client and enter the scopes and grant types after creating the client directly in the db
|
|
||||||
```text
|
|
||||||
scopes = email profile openid
|
|
||||||
grants = authorization_code
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
# Intro
|
|
||||||
Each organization are a collection of users and projects.
|
|
||||||
Users will be able to have multiple organizations and different roles in each of them. For example, a user can be an
|
|
||||||
admin in one organization and a member in another organization.
|
|
||||||
|
|
||||||
Each organization will have a unique slug that will consist of 4 lowercase letters and this slug will be used for the
|
|
||||||
database name of each project contained in an organization.
|
|
||||||
|
|
||||||
## Projects
|
|
||||||
Each project will have a unique name. Each project will be associated with an organization and will have a unique slug
|
|
||||||
that will come from the organization. The project will have a JSON field that will contain the different applications it has access to
|
|
||||||
|
|
||||||
## Organization Management
|
|
||||||
The organization management will have different features, such as creating an organization, inviting users to an organization,
|
|
||||||
managing the roles of users in an organization(admin or not), and deleting an organization.
|
|
||||||
|
|
||||||
### CRUD Operations
|
|
||||||
- **Create Organization**: Super Admin
|
|
||||||
- **Read Organization**: Super Admin, Admin and admin of the organization
|
|
||||||
- **Update Organization**: Super Admin, Admin
|
|
||||||
- **Delete Organization**: Super Admin
|
|
||||||
|
|
||||||
### User Management
|
|
||||||
- **Invite User**: Super Admin, Admin and admin of the organization
|
|
||||||
- **Remove User**: Super Admin, Admin and admin of the organization
|
|
||||||
- **Change User Role**: Super Admin, Admin and admin of the organization
|
|
||||||
- **List Users**: Super Admin, Admin and admin of the organization
|
|
||||||
- **Accept Invitation**: User
|
|
||||||
- **Decline Invitation**: User
|
|
||||||
|
|
||||||
### Project Management
|
|
||||||
- **Create Project**: Super Admin
|
|
||||||
- **Read Project**: Super Admin, Admin and admin of the organization
|
|
||||||
- **Update Project**: Super Admin
|
|
||||||
- **Delete Project**: Super Admin
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
# Intro
|
|
||||||
Roles will be split into two categories: **System Roles** and **Organizations Roles**.
|
|
||||||
System roles are global and apply to the entire system, while Organizations roles are specific to individual Organizations.
|
|
||||||
|
|
||||||
## System Roles
|
|
||||||
System roles are global and apply to the entire system. They include:
|
|
||||||
- **System Super Admin**: Has full access to all system features and settings. Can manage users, projects, organizations and applications. (SI)
|
|
||||||
- **System Admin**: Has access to most system features and settings. Can manage users, organizations, applications authorizations by projects. (BE)
|
|
||||||
- **System User**: Has limited access to system features and settings. Can view projects and applications, can manage own information, and organization where they are admin. (Others)
|
|
||||||
|
|
||||||
### System Super Admin
|
|
||||||
Get Access to the following with the following authorisations:
|
|
||||||
- **Users**: READ, CREATE, UPDATE, DELETE
|
|
||||||
- **Projects**: READ, CREATE, UPDATE, DELETE
|
|
||||||
- **Organizations**: READ, CREATE, UPDATE, DELETE
|
|
||||||
- **Applications**: READ, UPDATE
|
|
||||||
|
|
||||||
### System Admin
|
|
||||||
Get Access to the following with the following authorisations:
|
|
||||||
- **Users**: READ, CREATE, UPDATE, DELETE
|
|
||||||
- **Organizations**: READ, UPDATE
|
|
||||||
- **Applications**: READ
|
|
||||||
|
|
||||||
### System User
|
|
||||||
Get Access to the following with the following authorisations:
|
|
||||||
- **Users**: READ, UPDATE (own information only), READ (organization where they are admin), CREATE ( organization where they are admin), UPDATE (organization where they are admin), DELETE (organization where they are admin)
|
|
||||||
- **Projects**: READ ( of organization they are part of)
|
|
||||||
- **Organizations**: READ
|
|
||||||
- **Applications**: READ
|
|
||||||
|
|
||||||
## Organizations Roles
|
|
||||||
Organizations roles are specific to individual Organizations. They include:
|
|
||||||
- **Organization Admin**: Has full access to all organization features and settings. Can manage users of the organizations.
|
|
||||||
- **Organization User**: Has limited access to organization features and settings. Can view projects and applications, can manage own information
|
|
||||||
|
|
||||||
|
|
||||||
# Set up
|
|
||||||
Like for the sso, we need to create roles in the system. create the following command and the create the roles.
|
|
||||||
``` php
|
|
||||||
|
|
||||||
#[AsCommand(
|
|
||||||
name: 'app:create-role',
|
|
||||||
description: 'Creates a new role in the database'
|
|
||||||
)]
|
|
||||||
class CreateRoleCommand extends Command
|
|
||||||
{
|
|
||||||
private EntityManagerInterface $entityManager;
|
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $entityManager)
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
$this->entityManager = $entityManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this
|
|
||||||
->addArgument('name', InputArgument::REQUIRED, 'The name of the role'); // role name required
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$roleName = trim($input->getArgument('name'));
|
|
||||||
$roleName = strtoupper($roleName); // Normalize to uppercase
|
|
||||||
|
|
||||||
// Ensure not empty
|
|
||||||
if ($roleName === '') {
|
|
||||||
$output->writeln('<error>The role name cannot be empty</error>');
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if role already exists
|
|
||||||
$existing = $this->entityManager->getRepository(Roles::class)
|
|
||||||
->findOneBy(['name' => $roleName]);
|
|
||||||
|
|
||||||
if ($existing) {
|
|
||||||
$output->writeln("<comment>Role '{$roleName}' already exists.</comment>");
|
|
||||||
return Command::SUCCESS; // not failure, just redundant
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and persist new role
|
|
||||||
$role = new Roles();
|
|
||||||
$role->setName($roleName);
|
|
||||||
|
|
||||||
$this->entityManager->persist($role);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
$output->writeln("<info>Role '{$roleName}' created successfully!</info>");
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
```php
|
|
||||||
#[AsCommand(
|
|
||||||
name: 'app:delete-role',
|
|
||||||
description: 'Deletes a role from the database'
|
|
||||||
)]
|
|
||||||
class DeleteRoleCommand extends Command
|
|
||||||
{
|
|
||||||
private EntityManagerInterface $entityManager;
|
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $entityManager)
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
$this->entityManager = $entityManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this
|
|
||||||
->addArgument('name', InputArgument::REQUIRED, 'The name of the role to delete');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$roleName = trim($input->getArgument('name'));
|
|
||||||
$roleName = strtoupper($roleName); // Normalize to uppercase
|
|
||||||
|
|
||||||
if ($roleName === '') {
|
|
||||||
$output->writeln('<error>The role name cannot be empty</error>');
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the role
|
|
||||||
$role = $this->entityManager->getRepository(Roles::class)
|
|
||||||
->findOneBy(['name' => $roleName]);
|
|
||||||
|
|
||||||
if (!$role) {
|
|
||||||
$output->writeln("<error>Role '{$roleName}' not found.</error>");
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if role is being used (optional safety check)
|
|
||||||
$usageCount = $this->entityManager->getRepository(\App\Entity\UsersOrganizations::class)
|
|
||||||
->count(['role' => $role]);
|
|
||||||
|
|
||||||
if ($usageCount > 0) {
|
|
||||||
$output->writeln("<error>Cannot delete role '{$roleName}' - it is assigned to {$usageCount} user(s).</error>");
|
|
||||||
$output->writeln('<comment>Remove all assignments first, then try again.</comment>');
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirmation prompt
|
|
||||||
$helper = $this->getHelper('question');
|
|
||||||
$question = new ConfirmationQuestion(
|
|
||||||
"Are you sure you want to delete role '{$roleName}'? [y/N] ",
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$helper->ask($input, $output, $question)) {
|
|
||||||
$output->writeln('<comment>Operation cancelled.</comment>');
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the role
|
|
||||||
$this->entityManager->remove($role);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
$output->writeln("<info>Role '{$roleName}' deleted successfully!</info>");
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
``` bash
|
|
||||||
php bin/console app:create-role USER
|
|
||||||
php bin/console app:create-role ADMIN
|
|
||||||
```
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
# Documentation SSO/SLO - EasyPortal & EasyCheck
|
|
||||||
|
|
||||||
## Vue d'ensemble
|
|
||||||
|
|
||||||
Cette documentation décrit l'implémentation du **Single Sign-On (SSO)** et du **Single Logout (SLO)** entre deux applications Symfony :
|
|
||||||
- **EasyPortal** : Serveur d'autorisation OAuth2 (Identity Provider)
|
|
||||||
- **EasyCheck** : Application cliente OAuth2
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────┐ ┌─────────────────┐
|
|
||||||
│ EasyPortal │ │ EasyCheck │
|
|
||||||
│ (OAuth2 Server)│◄──────────────────►│ (OAuth2 Client) │
|
|
||||||
│ │ │ │
|
|
||||||
│ - Authentifie │ │ - Utilise le │
|
|
||||||
│ - Émet tokens │ │ token OAuth2 │
|
|
||||||
│ - Révoque │ │ - Valide token │
|
|
||||||
└─────────────────┘ └─────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Single Sign-On (SSO)
|
|
||||||
|
|
||||||
### Principe
|
|
||||||
|
|
||||||
L'utilisateur s'authentifie **une seule fois** sur le portail et accède ensuite à toutes les applications sans re-saisir ses identifiants.
|
|
||||||
|
|
||||||
### Flux d'authentification
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Utilisateur → EasyCheck
|
|
||||||
└─> Pas de session active
|
|
||||||
|
|
||||||
2. EasyCheck → Redirection vers EasyPortal
|
|
||||||
└─> /authorize?client_id=...&redirect_uri=...
|
|
||||||
|
|
||||||
3. Utilisateur → Connexion sur EasyPortal
|
|
||||||
└─> Login/Password ou session existante
|
|
||||||
|
|
||||||
4. EasyPortal → Redirection vers EasyCheck
|
|
||||||
└─> /sso_check?code=AUTHORIZATION_CODE
|
|
||||||
|
|
||||||
5. EasyCheck → Échange du code contre un token
|
|
||||||
└─> POST /token avec authorization_code
|
|
||||||
└─> Reçoit access_token + refresh_token
|
|
||||||
|
|
||||||
6. EasyCheck → Création de session locale
|
|
||||||
└─> Stockage du token en session
|
|
||||||
└─> Utilisateur connecté
|
|
||||||
```
|
|
||||||
|
|
||||||
## Single Logout (SLO)
|
|
||||||
|
|
||||||
### Principe
|
|
||||||
|
|
||||||
Lorsqu'un utilisateur se déconnecte d'une application, il est **automatiquement déconnecté de toutes les applications** SSO via un appel API asynchrone, évitant les boucles de redirections.
|
|
||||||
|
|
||||||
### Flux de déconnexion depuis EasyCheck
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Utilisateur → Clic "Déconnexion" sur EasyCheck
|
|
||||||
└─> GET /logout
|
|
||||||
|
|
||||||
2. Symfony → Invalide la session EasyCheck
|
|
||||||
└─> Session détruite, cookies supprimés
|
|
||||||
|
|
||||||
3. LogoutSubscriber → Interception de l'événement
|
|
||||||
└─> Détecte que ce n'est pas une déconnexion depuis le portail
|
|
||||||
|
|
||||||
4. EasyCheck → Redirection vers portail
|
|
||||||
└─> GET https://portail.../sso_logout?from_easycheck=1
|
|
||||||
|
|
||||||
5. EasyPortal → Révocation des tokens OAuth2
|
|
||||||
└─> Tous les access_token de l'utilisateur sont révoqués
|
|
||||||
└─> Paramètre redirect_app=easycheck propagé dans l'URL
|
|
||||||
|
|
||||||
6. EasyPortal → Redirection vers /logout
|
|
||||||
└─> GET /logout
|
|
||||||
|
|
||||||
7. Symfony → Invalide la session EasyPortal
|
|
||||||
└─> Session détruite
|
|
||||||
|
|
||||||
8. LogoutSubscriber → Redirection vers EasyCheck
|
|
||||||
└─> GET https://check.../logout?from_portal=1&redirect_app=easycheck
|
|
||||||
|
|
||||||
9. EasyCheck → Détecte from_portal=1
|
|
||||||
└─> Invalide la session (si elle existe encore)
|
|
||||||
└─> Redirection vers portail login avec redirect_app
|
|
||||||
|
|
||||||
10. EasyPortal → Affichage page login
|
|
||||||
└─> GET /login?redirect_app=easycheck
|
|
||||||
|
|
||||||
11. Utilisateur se reconnecte → LoginSubscriber détecte redirect_app=easycheck
|
|
||||||
└─> Redirection automatique vers EasyCheck /sso/login
|
|
||||||
```
|
|
||||||
|
|
||||||
### Flux de déconnexion depuis EasyPortal
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Utilisateur → Clic "Déconnexion" sur EasyPortal
|
|
||||||
└─> GET /sso_logout
|
|
||||||
|
|
||||||
2. EasyPortal → Révocation des tokens OAuth2
|
|
||||||
└─> Tous les access_token de l'utilisateur sont révoqués
|
|
||||||
|
|
||||||
3. EasyPortal → Redirection vers /logout
|
|
||||||
└─> GET /logout
|
|
||||||
|
|
||||||
4. Symfony → Invalide la session EasyPortal
|
|
||||||
└─> Session détruite
|
|
||||||
|
|
||||||
5. LogoutSubscriber → Redirection vers EasyCheck
|
|
||||||
└─> GET https://check.../logout?from_portal=1
|
|
||||||
|
|
||||||
6. EasyCheck → Détecte from_portal=1
|
|
||||||
└─> Invalide la session EasyCheck
|
|
||||||
└─> Session détruite, cookies supprimés
|
|
||||||
|
|
||||||
7. EasyCheck → Redirection finale vers portail
|
|
||||||
└─> GET https://portail.../login
|
|
||||||
```
|
|
||||||
|
|
||||||
## Variables d'environnement
|
|
||||||
|
|
||||||
### EasyCheck (.env)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# URL du serveur SSO (EasyPortal)
|
|
||||||
SSO_URL='https://portail.solutions-easy.moi'
|
|
||||||
|
|
||||||
# Configuration OAuth2
|
|
||||||
OAUTH_CLIENT_ID='easycheck-client-id'
|
|
||||||
OAUTH_CLIENT_SECRET='secret-key'
|
|
||||||
```
|
|
||||||
|
|
||||||
### EasyPortal (.env)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# URL de l'application cliente (EasyCheck)
|
|
||||||
EASYCHECK_URL='https://check.solutions-easy.moi'
|
|
||||||
|
|
||||||
# Configuration OAuth2 Server
|
|
||||||
OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.key
|
|
||||||
OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.key
|
|
||||||
OAUTH_PASSPHRASE='passphrase'
|
|
||||||
OAUTH_ENCRYPTION_KEY='encryption-key'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Système de redirection multi-applications
|
|
||||||
|
|
||||||
### Paramètre `redirect_app`
|
|
||||||
|
|
||||||
Pour gérer plusieurs applications SSO (EasyCheck, EasyAudit, EasyMaintenance, etc.), le système utilise un **paramètre URL `redirect_app`** au lieu de cookies.
|
|
||||||
|
|
||||||
**Avantages** :
|
|
||||||
- ✅ Fonctionne avec plusieurs onglets ouverts simultanément
|
|
||||||
- ✅ Chaque onglet garde son contexte de déconnexion
|
|
||||||
- ✅ Pas de conflit entre applications
|
|
||||||
- ✅ Facilement extensible pour de nouvelles applications
|
|
||||||
|
|
||||||
**Fonctionnement** :
|
|
||||||
1. Lors de la déconnexion depuis une application, elle envoie `redirect_app=nom_app`
|
|
||||||
2. Ce paramètre est propagé dans toutes les redirections de logout
|
|
||||||
3. Il arrive sur la page `/login?redirect_app=nom_app`
|
|
||||||
4. Après reconnexion, l'utilisateur est redirigé vers l'application d'origine
|
|
||||||
|
|
||||||
**Configuration dans LoginSubscriber** :
|
|
||||||
```php
|
|
||||||
$appUrls = [
|
|
||||||
'easycheck' => $easycheckUrl . '/sso/login',
|
|
||||||
'easyaudit' => $easyauditUrl . '/sso/login',
|
|
||||||
'easymaintenance' => $easymaintenanceUrl . '/sso/login',
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
## Endpoints
|
|
||||||
|
|
||||||
### EasyCheck - API Logout
|
|
||||||
|
|
||||||
**Route** : `POST /api/logout`
|
|
||||||
|
|
||||||
**Description** : Endpoint API pour invalider la session EasyCheck sans redirection (non utilisé actuellement, prévu pour usage futur).
|
|
||||||
|
|
||||||
**Réponse** :
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "Session invalidated successfully"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Points importants
|
|
||||||
|
|
||||||
### Sécurité
|
|
||||||
|
|
||||||
1. **CSRF désactivé sur logout** : Les routes de logout utilisent `enable_csrf: false` car ce sont des liens GET simples
|
|
||||||
2. **Tokens révoqués** : Lors du logout, tous les access_token de l'utilisateur sont révoqués côté portail
|
|
||||||
3. **Sessions invalidées** : Les sessions PHP sont complètement détruites des deux côtés
|
|
||||||
4. **Cookies supprimés** : Les cookies de session sont explicitement supprimés
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
1. **Pas de boucles infinies** : Utilisation du paramètre `from_portal` pour éviter les boucles de redirections
|
|
||||||
2. **Déconnexion bidirectionnelle** : Chaque application invalide la session de l'autre lors de la déconnexion
|
|
||||||
3. **Flux prévisible** : Chaque déconnexion suit un chemin clair avec des paramètres explicites
|
|
||||||
4. **Retour automatique** : Paramètre URL `redirect_app` pour rediriger l'utilisateur vers l'application d'origine après reconnexion
|
|
||||||
5. **Single Logout complet** : Toutes les sessions (portail + applications) sont toujours invalidées, quelle que soit l'origine de la déconnexion
|
|
||||||
6. **Multi-applications** : Support natif de plusieurs applications SSO sans conflit entre onglets
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
{
|
|
||||||
# Global options
|
|
||||||
frankenphp {
|
|
||||||
# Number of workers for better performance
|
|
||||||
num_threads {$NUM_THREADS:4}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Order directives properly
|
|
||||||
order mercure after encode
|
|
||||||
order php_server before file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTP server - HTTPS is handled by caddy-proxy
|
|
||||||
{$SERVER_NAME:80} {
|
|
||||||
# Root directory
|
|
||||||
root * /app/public
|
|
||||||
|
|
||||||
# Enable compression
|
|
||||||
encode zstd gzip
|
|
||||||
|
|
||||||
# Mercure hub configuration (built-in)
|
|
||||||
mercure {
|
|
||||||
# Publisher JWT key
|
|
||||||
publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {
|
|
||||||
algorithm hs256
|
|
||||||
}
|
|
||||||
# Subscriber JWT key
|
|
||||||
subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {
|
|
||||||
algorithm hs256
|
|
||||||
}
|
|
||||||
# Allow anonymous subscribers
|
|
||||||
anonymous
|
|
||||||
# CORS configuration
|
|
||||||
cors_origins *
|
|
||||||
}
|
|
||||||
|
|
||||||
# Client max body size (for uploads)
|
|
||||||
request_body {
|
|
||||||
max_size 20MB
|
|
||||||
}
|
|
||||||
|
|
||||||
# Security: Deny access to sensitive directories
|
|
||||||
@forbidden {
|
|
||||||
path /bin/* /config/* /src/* /templates/* /tests/* /translations/* /var/* /vendor/*
|
|
||||||
}
|
|
||||||
handle @forbidden {
|
|
||||||
respond "Access Denied" 404
|
|
||||||
}
|
|
||||||
|
|
||||||
# Security: Deny access to dot files (except .well-known for Mercure)
|
|
||||||
@dotfiles {
|
|
||||||
path */.*
|
|
||||||
not path /.well-known/*
|
|
||||||
}
|
|
||||||
handle @dotfiles {
|
|
||||||
respond "Access Denied" 404
|
|
||||||
}
|
|
||||||
|
|
||||||
# Cache static assets (30 days)
|
|
||||||
@static {
|
|
||||||
path *.jpg *.jpeg *.png *.gif *.ico *.css *.js *.svg *.woff *.woff2 *.ttf *.eot *.xlsx
|
|
||||||
}
|
|
||||||
handle @static {
|
|
||||||
header Cache-Control "public, max-age=2592000, no-transform"
|
|
||||||
file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
# PHP FrankenPHP handler
|
|
||||||
php_server {
|
|
||||||
# Resolve symlinks
|
|
||||||
resolve_root_symlink
|
|
||||||
}
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
log {
|
|
||||||
output file /var/log/caddy/access.log
|
|
||||||
format json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
{
|
|
||||||
# Global options
|
|
||||||
frankenphp {
|
|
||||||
# Number of workers for better performance
|
|
||||||
num_threads {$NUM_THREADS:4}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Order directives properly
|
|
||||||
order mercure after encode
|
|
||||||
order php_server before file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTP - redirect to HTTPS
|
|
||||||
http://{$SERVER_NAME:localhost} {
|
|
||||||
redir https://{host}{uri} permanent
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTPS server
|
|
||||||
https://{$SERVER_NAME:localhost} {
|
|
||||||
# Root directory
|
|
||||||
root * /app/public
|
|
||||||
|
|
||||||
# TLS configuration - Caddy will automatically obtain and renew Let's Encrypt certificates
|
|
||||||
tls {
|
|
||||||
protocols tls1.2 tls1.3
|
|
||||||
}
|
|
||||||
|
|
||||||
# Enable compression
|
|
||||||
encode zstd gzip
|
|
||||||
|
|
||||||
# Mercure hub configuration (built-in)
|
|
||||||
mercure {
|
|
||||||
# Publisher JWT key
|
|
||||||
publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {
|
|
||||||
algorithm hs256
|
|
||||||
}
|
|
||||||
# Subscriber JWT key
|
|
||||||
subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {
|
|
||||||
algorithm hs256
|
|
||||||
}
|
|
||||||
# Allow anonymous subscribers
|
|
||||||
anonymous
|
|
||||||
# Enable subscriptions
|
|
||||||
subscriptions
|
|
||||||
# CORS configuration
|
|
||||||
cors_origins *
|
|
||||||
}
|
|
||||||
|
|
||||||
# Client max body size (for uploads)
|
|
||||||
request_body {
|
|
||||||
max_size 20MB
|
|
||||||
}
|
|
||||||
|
|
||||||
# Security: Deny access to sensitive directories
|
|
||||||
@forbidden {
|
|
||||||
path /bin/* /config/* /src/* /templates/* /tests/* /translations/* /var/* /vendor/*
|
|
||||||
}
|
|
||||||
handle @forbidden {
|
|
||||||
respond "Access Denied" 404
|
|
||||||
}
|
|
||||||
|
|
||||||
# Security: Deny access to dot files (except .well-known for Mercure)
|
|
||||||
@dotfiles {
|
|
||||||
path */.*
|
|
||||||
not path /.well-known/*
|
|
||||||
}
|
|
||||||
handle @dotfiles {
|
|
||||||
respond "Access Denied" 404
|
|
||||||
}
|
|
||||||
|
|
||||||
# Cache static assets (30 days)
|
|
||||||
@static {
|
|
||||||
path *.jpg *.jpeg *.png *.gif *.ico *.css *.js *.svg *.woff *.woff2 *.ttf *.eot *.xlsx *.pdf
|
|
||||||
file
|
|
||||||
}
|
|
||||||
handle @static {
|
|
||||||
header Cache-Control "public, max-age=2592000, no-transform"
|
|
||||||
file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
# Serve files from /assets directory
|
|
||||||
handle /assets/* {
|
|
||||||
root * /app/public
|
|
||||||
file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
# PHP FrankenPHP handler
|
|
||||||
php_server {
|
|
||||||
# Resolve symlinks
|
|
||||||
resolve_root_symlink
|
|
||||||
}
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
log {
|
|
||||||
output file /app/var/log/access.log
|
|
||||||
format json
|
|
||||||
# Redact sensitive data
|
|
||||||
format filter {
|
|
||||||
request>uri query {
|
|
||||||
replace authorization REDACTED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
expose_php = 0
|
|
||||||
date.timezone = UTC
|
|
||||||
apc.enable_cli = 1
|
|
||||||
session.use_strict_mode = 1
|
|
||||||
zend.detect_unicode = 0
|
|
||||||
|
|
||||||
; https://symfony.com/doc/current/performance.html
|
|
||||||
realpath_cache_size = 4096K
|
|
||||||
realpath_cache_ttl = 600
|
|
||||||
opcache.interned_strings_buffer = 16
|
|
||||||
opcache.max_accelerated_files = 20000
|
|
||||||
opcache.memory_consumption = 256
|
|
||||||
opcache.enable_file_override = 1
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
; See https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host
|
|
||||||
; See https://github.com/docker/for-linux/issues/264
|
|
||||||
; The `client_host` below may optionally be replaced with `discover_client_host=yes`
|
|
||||||
; Add `start_with_request=yes` to start debug session on each request
|
|
||||||
xdebug.client_host = host.docker.internal
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
; https://symfony.com/doc/current/performance.html#use-the-opcache-class-preloading
|
|
||||||
opcache.preload_user = root
|
|
||||||
opcache.preload = /app/config/preload.php
|
|
||||||
; https://symfony.com/doc/current/performance.html#don-t-check-php-files-timestamps
|
|
||||||
opcache.validate_timestamps = 0
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
|
|
||||||
# Install the project the first time PHP is started
|
|
||||||
# After the installation, the following block can be deleted
|
|
||||||
if [ ! -f composer.json ]; then
|
|
||||||
rm -Rf tmp/
|
|
||||||
composer create-project "symfony/skeleton $SYMFONY_VERSION" tmp --stability="$STABILITY" --prefer-dist --no-progress --no-interaction --no-install
|
|
||||||
|
|
||||||
cd tmp
|
|
||||||
cp -Rp . ..
|
|
||||||
cd -
|
|
||||||
rm -Rf tmp/
|
|
||||||
|
|
||||||
composer require "php:>=$PHP_VERSION" runtime/frankenphp-symfony
|
|
||||||
composer config --json extra.symfony.docker 'true'
|
|
||||||
|
|
||||||
if grep -q ^DATABASE_URL= .env; then
|
|
||||||
echo 'To finish the installation please press Ctrl+C to stop Docker Compose and run: docker compose up --build --wait'
|
|
||||||
sleep infinity
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$(ls -A 'vendor/' 2>/dev/null)" ]; then
|
|
||||||
composer install --prefer-dist --no-progress --no-interaction
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Display information about the current project
|
|
||||||
# Or about an error in project initialization
|
|
||||||
php bin/console -V
|
|
||||||
|
|
||||||
if grep -q ^DATABASE_URL= .env; then
|
|
||||||
echo 'Waiting for database to be ready...'
|
|
||||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
|
|
||||||
until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
|
|
||||||
if [ $? -eq 255 ]; then
|
|
||||||
# If the Doctrine command exits with 255, an unrecoverable error occurred
|
|
||||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=0
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
|
|
||||||
echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
|
|
||||||
echo 'The database is not up or not reachable:'
|
|
||||||
echo "$DATABASE_ERROR"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo 'The database is now ready and reachable'
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$( find ./migrations -iname '*.php' -print -quit )" ]; then
|
|
||||||
php bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
|
||||||
setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
|
||||||
|
|
||||||
echo 'PHP app ready!'
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec docker-php-entrypoint "$@"
|
|
||||||
|
|
@ -35,42 +35,4 @@ return [
|
||||||
'version' => '5.3.5',
|
'version' => '5.3.5',
|
||||||
'type' => 'css',
|
'type' => 'css',
|
||||||
],
|
],
|
||||||
'choices.js' => [
|
|
||||||
'version' => '11.1.0',
|
|
||||||
],
|
|
||||||
'choices.js/public/assets/styles/choices.min.css' => [
|
|
||||||
'version' => '11.1.0',
|
|
||||||
'type' => 'css',
|
|
||||||
],
|
|
||||||
'quill' => [
|
|
||||||
'version' => '2.0.3',
|
|
||||||
],
|
|
||||||
'lodash-es' => [
|
|
||||||
'version' => '4.17.21',
|
|
||||||
],
|
|
||||||
'parchment' => [
|
|
||||||
'version' => '3.0.0',
|
|
||||||
],
|
|
||||||
'quill-delta' => [
|
|
||||||
'version' => '5.1.0',
|
|
||||||
],
|
|
||||||
'eventemitter3' => [
|
|
||||||
'version' => '5.0.1',
|
|
||||||
],
|
|
||||||
'fast-diff' => [
|
|
||||||
'version' => '1.3.0',
|
|
||||||
],
|
|
||||||
'lodash.clonedeep' => [
|
|
||||||
'version' => '4.5.0',
|
|
||||||
],
|
|
||||||
'lodash.isequal' => [
|
|
||||||
'version' => '4.5.0',
|
|
||||||
],
|
|
||||||
'tabulator-tables' => [
|
|
||||||
'version' => '6.3.1',
|
|
||||||
],
|
|
||||||
'tabulator-tables/dist/css/tabulator.min.css' => [
|
|
||||||
'version' => '6.3.1',
|
|
||||||
'type' => 'css',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<?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 Version20250709072959 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('DROP SEQUENCE subscriptions_id_seq CASCADE');
|
|
||||||
$this->addSql('ALTER TABLE subscriptions DROP CONSTRAINT fk_4778a0167b3b43d');
|
|
||||||
$this->addSql('DROP TABLE subscriptions');
|
|
||||||
}
|
|
||||||
|
|
||||||
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('CREATE SEQUENCE subscriptions_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
|
||||||
$this->addSql('CREATE TABLE subscriptions (id SERIAL NOT NULL, users_id INT NOT NULL, PRIMARY KEY(id))');
|
|
||||||
$this->addSql('CREATE INDEX idx_4778a0167b3b43d ON subscriptions (users_id)');
|
|
||||||
$this->addSql('ALTER TABLE subscriptions ADD CONSTRAINT fk_4778a0167b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
<?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 Version20250709073312 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
// this up() migration is auto-generated, please modify it to your needs
|
|
||||||
$this->addSql('CREATE TABLE subscriptions (id SERIAL NOT NULL, users_id INT NOT NULL, PRIMARY KEY(id))');
|
|
||||||
$this->addSql('CREATE INDEX IDX_4778A0167B3B43D ON subscriptions (users_id)');
|
|
||||||
$this->addSql('ALTER TABLE subscriptions ADD CONSTRAINT FK_4778A0167B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
|
||||||
}
|
|
||||||
|
|
||||||
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 subscriptions DROP CONSTRAINT FK_4778A0167B3B43D');
|
|
||||||
$this->addSql('DROP TABLE subscriptions');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
<?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 Version20250709073752 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
// this up() migration is auto-generated, please modify it to your needs
|
|
||||||
$this->addSql('CREATE TABLE user_tab (id SERIAL NOT NULL, users_id INT NOT NULL, ip_address VARCHAR(255) NOT NULL, tab_id VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
|
|
||||||
$this->addSql('CREATE INDEX IDX_98F5228767B3B43D ON user_tab (users_id)');
|
|
||||||
$this->addSql('ALTER TABLE user_tab ADD CONSTRAINT FK_98F5228767B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
|
||||||
}
|
|
||||||
|
|
||||||
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 user_tab DROP CONSTRAINT FK_98F5228767B3B43D');
|
|
||||||
$this->addSql('DROP TABLE user_tab');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<?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 Version20250709115309 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('DROP SEQUENCE subscriptions_id_seq CASCADE');
|
|
||||||
$this->addSql('ALTER TABLE subscriptions DROP CONSTRAINT fk_4778a0167b3b43d');
|
|
||||||
$this->addSql('DROP TABLE subscriptions');
|
|
||||||
}
|
|
||||||
|
|
||||||
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('CREATE SEQUENCE subscriptions_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
|
||||||
$this->addSql('CREATE TABLE subscriptions (id SERIAL NOT NULL, users_id INT NOT NULL, PRIMARY KEY(id))');
|
|
||||||
$this->addSql('CREATE INDEX idx_4778a0167b3b43d ON subscriptions (users_id)');
|
|
||||||
$this->addSql('ALTER TABLE subscriptions ADD CONSTRAINT fk_4778a0167b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
<?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 Version20250709120951 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 organizations ADD name VARCHAR(255) 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 organizations DROP name');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
<?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 Version20250709121023 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 organizations ADD name VARCHAR(255) DEFAULT 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 organizations DROP name');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
<?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 Version20250709141934 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 "user" ADD last_connection DATE DEFAULT NULL');
|
|
||||||
$this->addSql('COMMENT ON COLUMN "user".last_connection IS \'(DC2Type:date_immutable)\'');
|
|
||||||
}
|
|
||||||
|
|
||||||
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 "user" DROP last_connection');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<?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 Version20250710070735 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 "user" ALTER last_connection TYPE DATE');
|
|
||||||
$this->addSql('COMMENT ON COLUMN "user".last_connection IS 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 "user" ALTER last_connection TYPE DATE');
|
|
||||||
$this->addSql('COMMENT ON COLUMN "user".last_connection IS \'(DC2Type:date_immutable)\'');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
<?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 Version20250710071344 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 "user" ALTER last_connection TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
|
|
||||||
}
|
|
||||||
|
|
||||||
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 "user" ALTER last_connection TYPE DATE');
|
|
||||||
}
|
|
||||||
}
|
|
||||||