Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f797e307d | ||
| 89465f5cd5 | |||
|
|
0e68d9dbe7 | ||
| 19ac37fb3e | |||
|
|
7e967a1649 | ||
| f5ab0335f9 | |||
|
|
44e1e4a293 | ||
| ad92a4c434 | |||
|
|
be12175e17 | ||
| e8fc85c173 | |||
|
|
b39e6f81d8 | ||
| 28690be509 | |||
|
|
7f6634bec7 | ||
| b0b05970c1 | |||
|
|
57be0bbf85 | ||
| d85d1cc1d6 | |||
| 7a3e010e88 | |||
| 9529f4cf63 | |||
| 30038255da | |||
| 2844fea802 | |||
| 4234efdb50 |
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[{compose.yaml,compose.*.yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
28
.env
28
.env
@@ -21,3 +21,31 @@ JWT_COOKIE_TTL=86400
|
|||||||
DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
||||||
|
|
||||||
ENCRYPTION_KEY=change_me_in_env_local
|
ENCRYPTION_KEY=change_me_in_env_local
|
||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_SECRET=
|
||||||
|
APP_SHARE_DIR=var/share
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> symfony/routing ###
|
||||||
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||||
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||||
|
DEFAULT_URI=http://localhost
|
||||||
|
###< symfony/routing ###
|
||||||
|
|
||||||
|
###> doctrine/doctrine-bundle ###
|
||||||
|
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||||
|
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||||
|
#
|
||||||
|
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
|
||||||
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||||
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
||||||
|
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
|
||||||
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
|
###> malio/maintenance ###
|
||||||
|
SIRH_MAINTENANCE_PATH=/var/www/maintenance/sirh/maintenance.on
|
||||||
|
LESSTIME_MAINTENANCE_PATH=/var/www/maintenance/lesstime/maintenance.on
|
||||||
|
INVENTORY_MAINTENANCE_PATH=/var/www/maintenance/inventory/maintenance.on
|
||||||
|
###< malio/maintenance ###
|
||||||
|
|||||||
4
.env.dev
Normal file
4
.env.dev
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
APP_SECRET=51b43bf17ac894e44bd113e75892c8c5
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
3
.env.test
Normal file
3
.env.test
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# define your env variables for the test env here
|
||||||
|
KERNEL_CLASS='App\Kernel'
|
||||||
|
APP_SECRET='$ecretf0rt3st'
|
||||||
65
.gitea/workflows/auto-tag-develop.yml
Normal file
65
.gitea/workflows/auto-tag-develop.yml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
name: Auto Tag Develop
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tag:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- name: Create next tag from config/version.yaml
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Skip if current commit already has a vX.Y.Z tag
|
||||||
|
if git tag --points-at HEAD | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||||
|
echo "Tag already exists on this commit. Skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
changed_version=false
|
||||||
|
if git diff --name-only "${{ gitea.event.before }}" "${{ gitea.event.after }}" | grep -q '^config/version\.yaml$'; then
|
||||||
|
changed_version=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
read_version() {
|
||||||
|
awk -F': *' '/app\.version:/{print $2}' config/version.yaml | tr -d '[:space:]' | tr -d "'\""
|
||||||
|
}
|
||||||
|
|
||||||
|
if $changed_version; then
|
||||||
|
version="$(read_version)"
|
||||||
|
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "Invalid version in version.yaml: $version" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
last_tag="$(git tag -l 'v*' --sort=-v:refname | head -n1 || true)"
|
||||||
|
if [ -z "$last_tag" ]; then
|
||||||
|
version="0.1.0"
|
||||||
|
else
|
||||||
|
base="${last_tag#v}"
|
||||||
|
IFS='.' read -r major minor patch <<< "$base"
|
||||||
|
version="${major}.${minor}.$((patch + 1))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "parameters:\\n app.version: '%s'\\n" "$version" > config/version.yaml
|
||||||
|
git config user.name "gitea-actions"
|
||||||
|
git config user.email "gitea-actions@local"
|
||||||
|
git add config/version.yaml
|
||||||
|
git commit -m "chore: bump version to v$version" || true
|
||||||
|
git push origin develop || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
tag="v$version"
|
||||||
|
git tag "$tag"
|
||||||
|
git push origin "$tag"
|
||||||
30
.gitea/workflows/build-docker.yml
Normal file
30
.gitea/workflows/build-docker.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Build & Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login to Gitea Registry
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login gitea.malio.fr -u "${{ gitea.repository_owner }}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: |
|
||||||
|
docker build \
|
||||||
|
-f infra/prod/Dockerfile \
|
||||||
|
-t gitea.malio.fr/malio-dev/central:${{ github.ref_name }} \
|
||||||
|
-t gitea.malio.fr/malio-dev/central:latest \
|
||||||
|
.
|
||||||
|
|
||||||
|
- name: Push Docker image
|
||||||
|
run: |
|
||||||
|
docker push gitea.malio.fr/malio-dev/central:${{ github.ref_name }}
|
||||||
|
docker push gitea.malio.fr/malio-dev/central:latest
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -44,3 +44,8 @@ LOG/
|
|||||||
###> claude ###
|
###> claude ###
|
||||||
.claude/
|
.claude/
|
||||||
###< claude ###
|
###< claude ###
|
||||||
|
|
||||||
|
###> phpunit/phpunit ###
|
||||||
|
/phpunit.xml
|
||||||
|
/.phpunit.cache/
|
||||||
|
###< phpunit/phpunit ###
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Application de gestion du SI Malio. Monorepo Symfony 8 (API Platform 4) + Nuxt 4
|
|||||||
- **Backend** : PHP 8.4, Symfony 8.0, API Platform 4, Doctrine ORM, PostgreSQL 16
|
- **Backend** : PHP 8.4, Symfony 8.0, API Platform 4, Doctrine ORM, PostgreSQL 16
|
||||||
- **Frontend** : Nuxt 4 (SSR off / SPA), Vue 3, Pinia, Tailwind CSS, @malio/layer-ui, nuxt-toast, @nuxtjs/i18n, @nuxt/icon
|
- **Frontend** : Nuxt 4 (SSR off / SPA), Vue 3, Pinia, Tailwind CSS, @malio/layer-ui, nuxt-toast, @nuxtjs/i18n, @nuxt/icon
|
||||||
- **Auth** : JWT HTTP-only cookie (lexik/jwt-authentication-bundle), login à `/login_check`, cookie `BEARER`
|
- **Auth** : JWT HTTP-only cookie (lexik/jwt-authentication-bundle), login à `/login_check`, cookie `BEARER`
|
||||||
- **Docker** : PHP-FPM + Node 24, Nginx (port 8083), PostgreSQL (port 5436)
|
- **Docker** : PHP-FPM + Node 24, Nginx (port 8084), PostgreSQL (port 5436)
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ make fixtures
|
|||||||
|
|
||||||
## Accès
|
## Accès
|
||||||
|
|
||||||
- Frontend : http://localhost:8083
|
- Frontend : http://localhost:8084
|
||||||
- API : http://localhost:8083/api
|
- API : http://localhost:8084/api
|
||||||
- Dev Nuxt (hot reload) : http://localhost:3003
|
- Dev Nuxt (hot reload) : http://localhost:3003
|
||||||
- Login : `admin` / `admin`
|
- Login : `admin` / `admin`
|
||||||
|
|||||||
4
bin/phpunit
Executable file
4
bin/phpunit
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"symfony/monolog-bundle": "^4.0",
|
"symfony/monolog-bundle": "^4.0",
|
||||||
"symfony/property-access": "8.0.*",
|
"symfony/property-access": "8.0.*",
|
||||||
"symfony/property-info": "8.0.*",
|
"symfony/property-info": "8.0.*",
|
||||||
|
"symfony/rate-limiter": "8.0.*",
|
||||||
"symfony/runtime": "8.0.*",
|
"symfony/runtime": "8.0.*",
|
||||||
"symfony/security-bundle": "8.0.*",
|
"symfony/security-bundle": "8.0.*",
|
||||||
"symfony/serializer": "8.0.*",
|
"symfony/serializer": "8.0.*",
|
||||||
|
|||||||
11149
composer.lock
generated
Normal file
11149
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
config/applications.yaml
Normal file
5
config/applications.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
parameters:
|
||||||
|
app.managed_applications:
|
||||||
|
- { name: 'SIRH', slug: 'sirh', maintenance_path: '%env(SIRH_MAINTENANCE_PATH)%' }
|
||||||
|
- { name: 'Lesstime', slug: 'lesstime', maintenance_path: '%env(LESSTIME_MAINTENANCE_PATH)%' }
|
||||||
|
- { name: 'Inventory', slug: 'inventory', maintenance_path: '%env(INVENTORY_MAINTENANCE_PATH)%' }
|
||||||
@@ -1,25 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
|
|
||||||
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
|
|
||||||
use Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle;
|
|
||||||
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
|
|
||||||
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
|
|
||||||
use Nelmio\CorsBundle\NelmioCorsBundle;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
|
||||||
use Symfony\Bundle\MonologBundle\MonologBundle;
|
|
||||||
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
FrameworkBundle::class => ['all' => true],
|
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||||
SecurityBundle::class => ['all' => true],
|
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||||
DoctrineBundle::class => ['all' => true],
|
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||||
DoctrineMigrationsBundle::class => ['all' => true],
|
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
NelmioCorsBundle::class => ['all' => true],
|
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||||
ApiPlatformBundle::class => ['all' => true],
|
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||||
DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||||
LexikJWTAuthenticationBundle::class => ['all' => true],
|
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||||
MonologBundle::class => ['all' => true],
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
19
config/packages/cache.yaml
Normal file
19
config/packages/cache.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
framework:
|
||||||
|
cache:
|
||||||
|
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||||
|
#prefix_seed: your_vendor_name/app_name
|
||||||
|
|
||||||
|
# The "app" cache stores to the filesystem by default.
|
||||||
|
# The data in this cache should persist between deploys.
|
||||||
|
# Other options include:
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
#app: cache.adapter.redis
|
||||||
|
#default_redis_provider: redis://localhost
|
||||||
|
|
||||||
|
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||||
|
#app: cache.adapter.apcu
|
||||||
|
|
||||||
|
# Namespaced pools use the above "app" backend by default
|
||||||
|
#pools:
|
||||||
|
#my.dedicated.cache: null
|
||||||
6
config/packages/doctrine_migrations.yaml
Normal file
6
config/packages/doctrine_migrations.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
doctrine_migrations:
|
||||||
|
migrations_paths:
|
||||||
|
# namespace is arbitrary but should be different from App\Migrations
|
||||||
|
# as migrations classes should NOT be autoloaded
|
||||||
|
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||||
|
enable_profiler: false
|
||||||
55
config/packages/monolog.yaml
Normal file
55
config/packages/monolog.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
monolog:
|
||||||
|
channels:
|
||||||
|
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||||
|
|
||||||
|
when@dev:
|
||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
main:
|
||||||
|
type: stream
|
||||||
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
|
level: debug
|
||||||
|
channels: ["!event"]
|
||||||
|
console:
|
||||||
|
type: console
|
||||||
|
process_psr_3_messages: false
|
||||||
|
channels: ["!event", "!doctrine", "!console"]
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
main:
|
||||||
|
type: fingers_crossed
|
||||||
|
action_level: error
|
||||||
|
handler: nested
|
||||||
|
excluded_http_codes: [404, 405]
|
||||||
|
channels: ["!event"]
|
||||||
|
nested:
|
||||||
|
type: stream
|
||||||
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
|
level: debug
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
main:
|
||||||
|
type: fingers_crossed
|
||||||
|
action_level: error
|
||||||
|
handler: nested
|
||||||
|
excluded_http_codes: [404, 405]
|
||||||
|
channels: ["!deprecation"]
|
||||||
|
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||||
|
nested:
|
||||||
|
type: stream
|
||||||
|
path: php://stderr
|
||||||
|
level: debug
|
||||||
|
formatter: monolog.formatter.json
|
||||||
|
console:
|
||||||
|
type: console
|
||||||
|
process_psr_3_messages: false
|
||||||
|
channels: ["!event", "!doctrine"]
|
||||||
|
deprecation:
|
||||||
|
type: stream
|
||||||
|
channels: [deprecation]
|
||||||
|
path: php://stderr
|
||||||
|
formatter: monolog.formatter.json
|
||||||
11
config/packages/nyholm_psr7.yaml
Normal file
11
config/packages/nyholm_psr7.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
# Register nyholm/psr7 services for autowiring with PSR-17 (HTTP factories)
|
||||||
|
Psr\Http\Message\RequestFactoryInterface: '@nyholm.psr7.psr17_factory'
|
||||||
|
Psr\Http\Message\ResponseFactoryInterface: '@nyholm.psr7.psr17_factory'
|
||||||
|
Psr\Http\Message\ServerRequestFactoryInterface: '@nyholm.psr7.psr17_factory'
|
||||||
|
Psr\Http\Message\StreamFactoryInterface: '@nyholm.psr7.psr17_factory'
|
||||||
|
Psr\Http\Message\UploadedFileFactoryInterface: '@nyholm.psr7.psr17_factory'
|
||||||
|
Psr\Http\Message\UriFactoryInterface: '@nyholm.psr7.psr17_factory'
|
||||||
|
|
||||||
|
nyholm.psr7.psr17_factory:
|
||||||
|
class: Nyholm\Psr7\Factory\Psr17Factory
|
||||||
3
config/packages/property_info.yaml
Normal file
3
config/packages/property_info.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
framework:
|
||||||
|
property_info:
|
||||||
|
with_constructor_extractor: true
|
||||||
10
config/packages/routing.yaml
Normal file
10
config/packages/routing.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
framework:
|
||||||
|
router:
|
||||||
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||||
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||||
|
default_uri: '%env(DEFAULT_URI)%'
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
framework:
|
||||||
|
router:
|
||||||
|
strict_requirements: null
|
||||||
11
config/packages/validator.yaml
Normal file
11
config/packages/validator.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
framework:
|
||||||
|
validation:
|
||||||
|
# Enables validator auto-mapping support.
|
||||||
|
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
|
||||||
|
#auto_mapping:
|
||||||
|
# App\Entity\: []
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
framework:
|
||||||
|
validation:
|
||||||
|
not_compromised_password: false
|
||||||
1911
config/reference.php
Normal file
1911
config/reference.php
Normal file
File diff suppressed because it is too large
Load Diff
4
config/routes/framework.yaml
Normal file
4
config/routes/framework.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
when@dev:
|
||||||
|
_errors:
|
||||||
|
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
|
||||||
|
prefix: /_error
|
||||||
@@ -4,6 +4,7 @@ parameters:
|
|||||||
|
|
||||||
imports:
|
imports:
|
||||||
- { resource: version.yaml }
|
- { resource: version.yaml }
|
||||||
|
- { resource: applications.yaml }
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.1.0'
|
app.version: '0.1.8'
|
||||||
|
|||||||
265
doc/deployment-docker.md
Normal file
265
doc/deployment-docker.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
# Deploiement Docker — Central
|
||||||
|
|
||||||
|
## Pre-requis
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ubuntu
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y ca-certificates curl gnupg
|
||||||
|
sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
Se deconnecter/reconnecter pour que le groupe `docker` prenne effet.
|
||||||
|
|
||||||
|
### Nginx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y nginx
|
||||||
|
sudo systemctl enable nginx
|
||||||
|
sudo systemctl start nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL
|
||||||
|
|
||||||
|
PostgreSQL tourne sur l'hote ou dans un conteneur separe, accessible depuis Central via `host.docker.internal`.
|
||||||
|
|
||||||
|
Creer la base de donnees de production :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/postgres
|
||||||
|
docker compose exec postgres psql -U admin
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Si le user n'existe pas encore
|
||||||
|
CREATE USER malio WITH PASSWORD 'motdepasse';
|
||||||
|
|
||||||
|
-- Creer la base
|
||||||
|
CREATE DATABASE central_prod OWNER malio;
|
||||||
|
\q
|
||||||
|
```
|
||||||
|
|
||||||
|
## Premiere installation
|
||||||
|
|
||||||
|
### 1. Creer le dossier de deploiement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /var/www/central
|
||||||
|
sudo chown -R $(whoami):$(whoami) /var/www/central
|
||||||
|
cd /var/www/central
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Se connecter au registry Docker Gitea
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker login gitea.malio.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
- Username : le compte Gitea autorise au registry
|
||||||
|
- Password : le token registry
|
||||||
|
|
||||||
|
### 3. Creer les fichiers de deploiement
|
||||||
|
|
||||||
|
Copier les fichiers du repo :
|
||||||
|
|
||||||
|
- `infra/prod/docker-compose.yml`
|
||||||
|
- `infra/prod/deploy.sh`
|
||||||
|
|
||||||
|
Option equivalent ecrite en clair pour `docker-compose.yml` :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: gitea.malio.fr/malio-dev/central:${CENTRAL_IMAGE_TAG:-latest}
|
||||||
|
container_name: central-app
|
||||||
|
env_file: .env
|
||||||
|
ports:
|
||||||
|
- "8084:80"
|
||||||
|
volumes:
|
||||||
|
- ./config/jwt:/var/www/html/config/jwt:ro
|
||||||
|
- ./uploads:/var/www/html/var/uploads
|
||||||
|
- /var/www/sirh:/var/www/maintenance/sirh
|
||||||
|
- /var/www/lesstime:/var/www/maintenance/lesstime
|
||||||
|
- /var/www/inventory:/var/www/maintenance/inventory
|
||||||
|
- /var/www/ferme:/var/www/maintenance/ferme
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
Rendre le script executable :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp /chemin/vers/le/repo/Central/infra/prod/deploy.sh ./deploy.sh
|
||||||
|
chmod +x deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Creer le fichier `.env`
|
||||||
|
|
||||||
|
Exemple minimal de production :
|
||||||
|
|
||||||
|
```env
|
||||||
|
APP_ENV=prod
|
||||||
|
APP_DEBUG=0
|
||||||
|
APP_SECRET=<generer avec: openssl rand -hex 32>
|
||||||
|
|
||||||
|
DATABASE_URL="postgresql://malio:motdepasse@host.docker.internal:5432/central_prod?serverVersion=16&charset=utf8"
|
||||||
|
|
||||||
|
DEFAULT_URI=http://central.malio-dev.fr
|
||||||
|
APP_SHARE_DIR=var/share
|
||||||
|
|
||||||
|
CORS_ALLOW_ORIGIN='^http://central\.malio-dev\.fr$'
|
||||||
|
|
||||||
|
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||||
|
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||||
|
JWT_PASSPHRASE=<generer avec: openssl rand -hex 32>
|
||||||
|
JWT_COOKIE_SECURE=0
|
||||||
|
JWT_TOKEN_TTL=86400
|
||||||
|
JWT_COOKIE_TTL=86400
|
||||||
|
|
||||||
|
ENCRYPTION_KEY=<generer avec: openssl rand -hex 32>
|
||||||
|
|
||||||
|
SIRH_MAINTENANCE_PATH=/var/www/maintenance/sirh/maintenance.on
|
||||||
|
LESSTIME_MAINTENANCE_PATH=/var/www/maintenance/lesstime/maintenance.on
|
||||||
|
INVENTORY_MAINTENANCE_PATH=/var/www/maintenance/inventory/maintenance.on
|
||||||
|
FERME_MAINTENANCE_PATH=/var/www/maintenance/ferme/maintenance.on
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Generer les cles JWT
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p config/jwt
|
||||||
|
openssl genpkey -algorithm RSA -out config/jwt/private.pem -pkeyopt rsa_keygen_bits:4096
|
||||||
|
openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem
|
||||||
|
sudo chown 33:33 config/jwt/private.pem config/jwt/public.pem
|
||||||
|
sudo chmod 644 config/jwt/private.pem config/jwt/public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Creer les dossiers persistants
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Verifier l'acces aux apps managées
|
||||||
|
|
||||||
|
Central pilote les fichiers `maintenance.on` des autres projets via des volumes montes en lecture/ecriture.
|
||||||
|
|
||||||
|
Verifier que les dossiers existent :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -ld /var/www/sirh /var/www/lesstime /var/www/inventory /var/www/ferme
|
||||||
|
```
|
||||||
|
|
||||||
|
Si Central ne peut pas ecrire `maintenance.on`, il faudra ajuster les permissions sur ces dossiers pour que le processus du conteneur puisse creer/supprimer ce fichier.
|
||||||
|
|
||||||
|
### 8. Configurer Nginx systeme
|
||||||
|
|
||||||
|
Creer `/etc/nginx/sites-available/central.conf` :
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name central.malio-dev.fr;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8084;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
client_max_body_size 55m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Activer le site :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ln -sf /etc/nginx/sites-available/central.conf /etc/nginx/sites-enabled/central.conf
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Deployer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/central
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Le script :
|
||||||
|
|
||||||
|
- pull l'image Docker
|
||||||
|
- redemarre le conteneur
|
||||||
|
- lance les migrations Doctrine
|
||||||
|
- vide et rechauffe le cache Symfony
|
||||||
|
|
||||||
|
## Deployer une nouvelle version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/central
|
||||||
|
./deploy.sh # latest
|
||||||
|
./deploy.sh v0.1.0 # version specifique
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification apres deploiement
|
||||||
|
|
||||||
|
1. Ouvrir `http://central.malio-dev.fr`
|
||||||
|
2. Se connecter avec un compte admin
|
||||||
|
3. Verifier que la page Applications charge
|
||||||
|
4. Activer la maintenance sur SIRH
|
||||||
|
5. Verifier que `https://sirh.malio-dev.fr` renvoie la page de maintenance
|
||||||
|
6. Desactiver la maintenance depuis Central
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
### Image seule
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/central
|
||||||
|
./deploy.sh v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Avec rollback de migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/central
|
||||||
|
sudo docker compose exec -T -u www-data app php bin/console doctrine:migrations:migrate prev --no-interaction
|
||||||
|
./deploy.sh v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Voir les logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/central
|
||||||
|
sudo docker compose logs -f
|
||||||
|
sudo docker compose logs -f --tail=100
|
||||||
|
```
|
||||||
|
|
||||||
|
Logs Symfony :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/central
|
||||||
|
sudo docker compose exec -T app cat var/log/prod.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure finale du dossier
|
||||||
|
|
||||||
|
```text
|
||||||
|
/var/www/central/
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── deploy.sh
|
||||||
|
├── .env
|
||||||
|
├── config/jwt/
|
||||||
|
│ ├── private.pem
|
||||||
|
│ └── public.pem
|
||||||
|
└── uploads/
|
||||||
|
```
|
||||||
@@ -38,7 +38,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- php
|
- php
|
||||||
ports:
|
ports:
|
||||||
- "8083:80"
|
- "8084:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/var/www/html:ro
|
- ./:/var/www/html:ro
|
||||||
- ./infra/dev/nginx.conf:/etc/nginx/conf.d/central.conf:ro
|
- ./infra/dev/nginx.conf:/etc/nginx/conf.d/central.conf:ro
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function useAppVersion() {
|
|||||||
if (version.value) {
|
if (version.value) {
|
||||||
return version.value
|
return version.value
|
||||||
}
|
}
|
||||||
const response = await api.get<{ version: string }>('version', {}, {
|
const response = await api.get<{ version: string }>('/version', {}, {
|
||||||
toast: false
|
toast: false
|
||||||
})
|
})
|
||||||
version.value = response.version
|
version.value = response.version
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
"patch": "Impossible de mettre à jour la ressource.",
|
"patch": "Impossible de mettre à jour la ressource.",
|
||||||
"delete": "Impossible de supprimer la ressource."
|
"delete": "Impossible de supprimer la ressource."
|
||||||
},
|
},
|
||||||
|
"applications": {
|
||||||
|
"activateMaintenance": "Impossible d'activer le mode maintenance.",
|
||||||
|
"deactivateMaintenance": "Impossible de désactiver le mode maintenance."
|
||||||
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": "Identifiants invalides.",
|
"login": "Identifiants invalides.",
|
||||||
"logout": "Impossible de se déconnecter.",
|
"logout": "Impossible de se déconnecter.",
|
||||||
@@ -14,6 +18,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
|
"applications": {
|
||||||
|
"activateMaintenance": "Le mode maintenance a été activé.",
|
||||||
|
"deactivateMaintenance": "Le mode maintenance a été désactivé."
|
||||||
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": "Connexion réussie.",
|
"login": "Connexion réussie.",
|
||||||
"logout": "Déconnexion réussie."
|
"logout": "Déconnexion réussie."
|
||||||
@@ -21,5 +29,29 @@
|
|||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Tableau de bord"
|
"title": "Tableau de bord"
|
||||||
|
},
|
||||||
|
"applications": {
|
||||||
|
"eyebrow": "Pilotage centralise",
|
||||||
|
"title": "Supervision des applications",
|
||||||
|
"description": "Active ou desactive le mode maintenance sans te connecter a chaque projet. Chaque action pilote le fichier de maintenance de l'application cible.",
|
||||||
|
"listTitle": "Applications managees",
|
||||||
|
"listDescription": "L'etat affiche correspond au trigger de maintenance present sur le serveur de production.",
|
||||||
|
"emptyTitle": "Aucune application disponible",
|
||||||
|
"emptyDescription": "La configuration backend ne retourne encore aucune application geree.",
|
||||||
|
"status": {
|
||||||
|
"active": "Maintenance active",
|
||||||
|
"inactive": "En ligne"
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"activeDescription": "Les utilisateurs voient actuellement la page de maintenance de cette application.",
|
||||||
|
"inactiveDescription": "L'application repond normalement et le trigger de maintenance est absent.",
|
||||||
|
"triggerFile": "Fichier trigger maintenance.on"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"refresh": "Rafraichir",
|
||||||
|
"activate": "Activer la maintenance",
|
||||||
|
"deactivate": "Desactiver la maintenance",
|
||||||
|
"pending": "Mise a jour en cours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14853
frontend/package-lock.json
generated
Normal file
14853
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,176 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flex h-full flex-col gap-6 pb-10">
|
||||||
<h1 class="text-2xl font-bold text-neutral-900">Tableau de bord</h1>
|
<section class="flex flex-col gap-2">
|
||||||
<p class="mt-4 text-neutral-600">Bienvenue sur Central.</p>
|
<h1 class="text-2xl font-bold text-neutral-900 sm:text-3xl">
|
||||||
|
{{ t('applications.title') }}
|
||||||
|
</h1>
|
||||||
|
<p class="max-w-3xl text-sm leading-6 text-neutral-500 sm:text-base">
|
||||||
|
{{ t('applications.description') }}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="min-h-0 rounded-3xl border border-primary-500/15 bg-white shadow-sm">
|
||||||
|
<header class="flex flex-col gap-4 border-b border-primary-500/10 px-6 py-5 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-bold text-primary-500">
|
||||||
|
{{ t('applications.listTitle') }}
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-neutral-500">
|
||||||
|
{{ t('applications.listDescription') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center rounded-xl border border-primary-500/20 px-4 py-2 text-sm font-semibold text-primary-500 transition-colors hover:bg-tertiary-500 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="loadApplications()"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="mdi:refresh"
|
||||||
|
size="18"
|
||||||
|
class="mr-2"
|
||||||
|
:class="loading ? 'animate-spin' : ''"
|
||||||
|
/>
|
||||||
|
{{ t('applications.actions.refresh') }}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div v-if="loading" class="grid gap-4 p-6 xl:grid-cols-2">
|
||||||
|
<div
|
||||||
|
v-for="index in 4"
|
||||||
|
:key="index"
|
||||||
|
class="animate-pulse rounded-2xl border border-primary-500/10 p-5"
|
||||||
|
>
|
||||||
|
<div class="h-4 w-24 rounded bg-neutral-200" />
|
||||||
|
<div class="mt-4 h-7 w-40 rounded bg-neutral-200" />
|
||||||
|
<div class="mt-3 h-4 w-full rounded bg-neutral-100" />
|
||||||
|
<div class="mt-6 h-11 w-44 rounded-xl bg-neutral-200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="applications.length === 0" class="px-6 py-12 text-center">
|
||||||
|
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-tertiary-500 text-primary-500">
|
||||||
|
<Icon name="mdi:server-off" size="28" />
|
||||||
|
</div>
|
||||||
|
<h3 class="mt-4 text-lg font-bold text-primary-500">
|
||||||
|
{{ t('applications.emptyTitle') }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-2 text-sm text-neutral-500">
|
||||||
|
{{ t('applications.emptyDescription') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid gap-4 p-6 xl:grid-cols-2">
|
||||||
|
<article
|
||||||
|
v-for="application in applications"
|
||||||
|
:key="application.slug"
|
||||||
|
class="flex h-full min-w-0 flex-col rounded-2xl border p-5 transition-colors"
|
||||||
|
:class="application.maintenance ? 'border-red-200 bg-red-50/60' : 'border-emerald-200 bg-emerald-50/60'"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-neutral-500">
|
||||||
|
{{ application.slug }}
|
||||||
|
</p>
|
||||||
|
<h3 class="mt-2 break-words text-xl font-bold text-primary-500">
|
||||||
|
{{ application.name }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="inline-flex w-fit items-center rounded-full px-3 py-1 text-xs font-bold"
|
||||||
|
:class="application.maintenance ? 'bg-red-100 text-red-700' : 'bg-emerald-100 text-emerald-700'"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mr-2 h-2.5 w-2.5 rounded-full"
|
||||||
|
:class="application.maintenance ? 'bg-red-500' : 'bg-emerald-500'"
|
||||||
|
/>
|
||||||
|
{{ application.maintenance ? t('applications.status.active') : t('applications.status.inactive') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-4 flex-1 text-sm leading-6 text-neutral-600">
|
||||||
|
{{
|
||||||
|
application.maintenance
|
||||||
|
? t('applications.card.activeDescription')
|
||||||
|
: t('applications.card.inactiveDescription')
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-6 flex flex-col gap-3 border-t border-black/5 pt-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<p class="text-xs font-medium uppercase tracking-[0.14em] text-neutral-400">
|
||||||
|
{{ t('applications.card.triggerFile') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="inline-flex w-full items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold text-white transition-opacity disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto sm:min-w-[220px]"
|
||||||
|
:class="application.maintenance ? 'bg-red-600 hover:bg-red-700' : 'bg-primary-500 hover:bg-primary-600'"
|
||||||
|
:disabled="Boolean(pendingBySlug[application.slug])"
|
||||||
|
@click="toggleMaintenance(application)"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
:name="pendingBySlug[application.slug] ? 'mdi:loading' : (application.maintenance ? 'mdi:shield-check-outline' : 'mdi:alert-outline')"
|
||||||
|
size="18"
|
||||||
|
class="mr-2"
|
||||||
|
:class="pendingBySlug[application.slug] ? 'animate-spin' : ''"
|
||||||
|
/>
|
||||||
|
{{
|
||||||
|
pendingBySlug[application.slug]
|
||||||
|
? t('applications.actions.pending')
|
||||||
|
: application.maintenance
|
||||||
|
? t('applications.actions.deactivate')
|
||||||
|
: t('applications.actions.activate')
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { ManagedApplication } from '~/services/dto/managed-application'
|
||||||
|
import { getManagedApplications, setApplicationMaintenance } from '~/services/managed-applications'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const applications = ref<ManagedApplication[]>([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const pendingBySlug = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
async function loadApplications() {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
applications.value = await getManagedApplications()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleMaintenance(application: ManagedApplication) {
|
||||||
|
pendingBySlug.value = {
|
||||||
|
...pendingBySlug.value,
|
||||||
|
[application.slug]: true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedApplication = await setApplicationMaintenance(application.slug, !application.maintenance)
|
||||||
|
applications.value = applications.value.map((item) => item.slug === updatedApplication.slug ? updatedApplication : item)
|
||||||
|
} finally {
|
||||||
|
pendingBySlug.value = {
|
||||||
|
...pendingBySlug.value,
|
||||||
|
[application.slug]: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadApplications()
|
||||||
|
})
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Tableau de bord'
|
title: 'Applications'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mx-auto w-full max-w-lg">
|
<div class="mx-auto w-full max-w-lg">
|
||||||
<span
|
<div class="flex flex-col items-center justify-center">
|
||||||
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
|
<span
|
||||||
>
|
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
|
||||||
<img src="/malio.png" alt="Logo" class="w-[150px]"/>
|
>
|
||||||
</span>
|
<img src="/malio.png" alt="Logo" class="w-[150px]"/>
|
||||||
|
</span>
|
||||||
|
<h1 class="mt-2 text-3xl font-bold tracking-wide text-neutral-800">Central</h1>
|
||||||
|
</div>
|
||||||
<form
|
<form
|
||||||
class="mt-8 space-y-6 rounded-lg border border-neutral-200 bg-white p-6 shadow-sm"
|
class="mt-8 space-y-6 rounded-lg border border-neutral-200 bg-white p-6 shadow-sm"
|
||||||
@submit.prevent="handleSubmit"
|
@submit.prevent="handleSubmit"
|
||||||
|
|||||||
10
frontend/services/dto/managed-application.ts
Normal file
10
frontend/services/dto/managed-application.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export type ManagedApplication = {
|
||||||
|
slug: string
|
||||||
|
name: string
|
||||||
|
maintenance: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ManagedApplicationCollection = {
|
||||||
|
'hydra:member'?: ManagedApplication[]
|
||||||
|
member?: ManagedApplication[]
|
||||||
|
}
|
||||||
33
frontend/services/managed-applications.ts
Normal file
33
frontend/services/managed-applications.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { ManagedApplication, ManagedApplicationCollection } from './dto/managed-application'
|
||||||
|
|
||||||
|
function normalizeManagedApplications(response: ManagedApplication[] | ManagedApplicationCollection): ManagedApplication[] {
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
return response['hydra:member'] ?? response.member ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getManagedApplications(): Promise<ManagedApplication[]> {
|
||||||
|
const api = useApi()
|
||||||
|
const response = await api.get<ManagedApplication[] | ManagedApplicationCollection>('/applications')
|
||||||
|
|
||||||
|
return normalizeManagedApplications(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setApplicationMaintenance(slug: string, maintenance: boolean) {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
return api.post<ManagedApplication>(
|
||||||
|
`/applications/${slug}/maintenance`,
|
||||||
|
{ maintenance },
|
||||||
|
{
|
||||||
|
toastSuccessKey: maintenance
|
||||||
|
? 'success.applications.activateMaintenance'
|
||||||
|
: 'success.applications.deactivateMaintenance',
|
||||||
|
toastErrorKey: maintenance
|
||||||
|
? 'errors.applications.activateMaintenance'
|
||||||
|
: 'errors.applications.deactivateMaintenance'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
82
infra/prod/Dockerfile
Normal file
82
infra/prod/Dockerfile
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# --- Stage 1: Build backend ---
|
||||||
|
FROM php:8.4-cli AS backend-build
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libicu-dev libpq-dev libpng-dev libzip-dev libxml2-dev \
|
||||||
|
unzip curl git \
|
||||||
|
&& docker-php-ext-install -j$(nproc) intl pdo_pgsql zip gd opcache \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY composer.json composer.lock symfony.lock ./
|
||||||
|
RUN APP_ENV=prod APP_DEBUG=0 composer install --no-dev --no-scripts --no-interaction
|
||||||
|
|
||||||
|
COPY bin bin/
|
||||||
|
COPY config config/
|
||||||
|
COPY migrations migrations/
|
||||||
|
COPY public public/
|
||||||
|
COPY src src/
|
||||||
|
|
||||||
|
RUN composer dump-autoload --optimize --no-dev
|
||||||
|
|
||||||
|
# --- Stage 2: Build frontend ---
|
||||||
|
FROM node:lts-alpine AS frontend-build
|
||||||
|
|
||||||
|
WORKDIR /app/frontend
|
||||||
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY frontend/ ./
|
||||||
|
ENV CI=1 \
|
||||||
|
NUXT_TELEMETRY_DISABLED=1 \
|
||||||
|
NUXT_PUBLIC_API_BASE=/api \
|
||||||
|
NUXT_PUBLIC_APP_BASE=/
|
||||||
|
RUN npm run generate
|
||||||
|
|
||||||
|
# --- Stage 3: Production image ---
|
||||||
|
FROM php:8.4-fpm AS production
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libicu-dev libpq-dev libpng-dev libzip-dev libxml2-dev \
|
||||||
|
nginx supervisor \
|
||||||
|
&& docker-php-ext-install -j$(nproc) intl pdo_pgsql zip gd opcache \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# PHP production config
|
||||||
|
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||||
|
|
||||||
|
# PHP-FPM: forward worker output to stderr for docker logs
|
||||||
|
RUN echo "catch_workers_output = yes" >> /usr/local/etc/php-fpm.d/www.conf \
|
||||||
|
&& echo "decorate_workers_output = no" >> /usr/local/etc/php-fpm.d/www.conf
|
||||||
|
|
||||||
|
# Nginx: log to stdout/stderr
|
||||||
|
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
|
||||||
|
&& ln -sf /dev/stderr /var/log/nginx/error.log
|
||||||
|
|
||||||
|
# Remove default nginx site
|
||||||
|
RUN rm -f /etc/nginx/sites-enabled/default
|
||||||
|
|
||||||
|
# Configs
|
||||||
|
COPY infra/prod/supervisord.conf /etc/supervisor/conf.d/app.conf
|
||||||
|
COPY infra/prod/nginx.conf /etc/nginx/sites-enabled/central.conf
|
||||||
|
|
||||||
|
# Backend from stage 1
|
||||||
|
COPY --from=backend-build /app /var/www/html
|
||||||
|
|
||||||
|
# Frontend from stage 2
|
||||||
|
COPY --from=frontend-build /app/frontend/.output/public /var/www/html/frontend/.output/public
|
||||||
|
|
||||||
|
# Symfony needs a .env file to boot (variables are overridden by env_file in docker-compose)
|
||||||
|
RUN echo "APP_ENV=prod" > /var/www/html/.env
|
||||||
|
|
||||||
|
# Permissions
|
||||||
|
RUN mkdir -p /var/www/html/var /var/www/html/var/uploads \
|
||||||
|
/var/www/maintenance/sirh /var/www/maintenance/lesstime /var/www/maintenance/inventory \
|
||||||
|
&& chown -R www-data:www-data /var/www/html/var /var/www/maintenance
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["supervisord", "-n", "-c", "/etc/supervisor/conf.d/app.conf"]
|
||||||
28
infra/prod/deploy.sh
Executable file
28
infra/prod/deploy.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
TAG="${1:-latest}"
|
||||||
|
export CENTRAL_IMAGE_TAG="$TAG"
|
||||||
|
|
||||||
|
echo "==> Deploying central:${TAG}..."
|
||||||
|
|
||||||
|
echo "==> Pulling image..."
|
||||||
|
sudo docker compose pull
|
||||||
|
|
||||||
|
echo "==> Starting container..."
|
||||||
|
sudo docker compose up -d
|
||||||
|
|
||||||
|
echo "==> Waiting for container to be ready..."
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
echo "==> Running migrations..."
|
||||||
|
sudo docker compose exec -T -u www-data app php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
echo "==> Clearing cache..."
|
||||||
|
sudo docker compose exec -T -u www-data app php bin/console cache:clear --env=prod
|
||||||
|
sudo docker compose exec -T -u www-data app php bin/console cache:warmup --env=prod
|
||||||
|
|
||||||
|
VERSION=$(sudo docker compose exec -T app cat config/version.yaml | grep 'app.version' | awk -F"'" '{print $2}')
|
||||||
|
echo "==> Deployed v${VERSION}"
|
||||||
17
infra/prod/docker-compose.yml
Normal file
17
infra/prod/docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: gitea.malio.fr/malio-dev/central:${CENTRAL_IMAGE_TAG:-latest}
|
||||||
|
container_name: central-app
|
||||||
|
env_file: .env
|
||||||
|
ports:
|
||||||
|
- "8084:80"
|
||||||
|
volumes:
|
||||||
|
- ./config/jwt:/var/www/html/config/jwt:ro
|
||||||
|
- ./uploads:/var/www/html/var/uploads
|
||||||
|
- /var/www/sirh:/var/www/maintenance/sirh
|
||||||
|
- /var/www/lesstime:/var/www/maintenance/lesstime
|
||||||
|
- /var/www/inventory:/var/www/maintenance/inventory
|
||||||
|
- /var/www/ferme:/var/www/maintenance/ferme
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
restart: unless-stopped
|
||||||
14
infra/prod/nginx-proxy.conf
Normal file
14
infra/prod/nginx-proxy.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name central.malio-dev.fr;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8084;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
client_max_body_size 55m;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
infra/prod/nginx.conf
Normal file
48
infra/prod/nginx.conf
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name central.malio-dev.fr;
|
||||||
|
|
||||||
|
root /var/www/html/frontend/.output/public;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
client_max_body_size 55m;
|
||||||
|
|
||||||
|
access_log /dev/stdout;
|
||||||
|
error_log /dev/stderr;
|
||||||
|
|
||||||
|
location ^~ /api/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /bundles/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /api/login_check {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
||||||
|
fastcgi_param SCRIPT_NAME /index.php;
|
||||||
|
fastcgi_param PATH_INFO /login_check;
|
||||||
|
fastcgi_param REQUEST_URI /login_check;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/index\.php(/|$) {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
infra/prod/supervisord.conf
Normal file
28
infra/prod/supervisord.conf
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/dev/null
|
||||||
|
logfile_maxbytes=0
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php-fpm]
|
||||||
|
command=php-fpm -F
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
stopasgroup=true
|
||||||
|
stopsignal=QUIT
|
||||||
|
|
||||||
|
[program:nginx]
|
||||||
|
command=nginx -g "daemon off;"
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
stopasgroup=true
|
||||||
|
stopsignal=QUIT
|
||||||
0
migrations/.gitignore
vendored
Normal file
0
migrations/.gitignore
vendored
Normal file
32
migrations/Version20260403083313.php
Normal file
32
migrations/Version20260403083313.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260403083313 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" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, username VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649F85E0677 ON "user" (username)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('DROP TABLE "user"');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,38 @@
|
|||||||
<ini name="error_reporting" value="-1" />
|
<ini name="error_reporting" value="-1" />
|
||||||
<server name="APP_ENV" value="test" force="true" />
|
<server name="APP_ENV" value="test" force="true" />
|
||||||
<server name="SHELL_VERBOSITY" value="-1" />
|
<server name="SHELL_VERBOSITY" value="-1" />
|
||||||
|
|
||||||
|
<!-- ###+ symfony/framework-bundle ### -->
|
||||||
|
<env name="APP_ENV" value="dev"/>
|
||||||
|
<env name="APP_SECRET" value=""/>
|
||||||
|
<env name="APP_SHARE_DIR" value="var/share"/>
|
||||||
|
<!-- ###- symfony/framework-bundle ### -->
|
||||||
|
|
||||||
|
<!-- ###+ symfony/routing ### -->
|
||||||
|
<!-- Configure how to generate URLs in non-HTTP contexts, such as CLI commands. -->
|
||||||
|
<!-- See https://symfony.com/doc/current/routing.html#generating-urls-in-commands -->
|
||||||
|
<env name="DEFAULT_URI" value="http://localhost"/>
|
||||||
|
<!-- ###- symfony/routing ### -->
|
||||||
|
|
||||||
|
<!-- ###+ doctrine/doctrine-bundle ### -->
|
||||||
|
<!-- Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url -->
|
||||||
|
<!-- IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db" -->
|
||||||
|
<!-- DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4" -->
|
||||||
|
<!-- DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" -->
|
||||||
|
<env name="DATABASE_URL" value="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"/>
|
||||||
|
<!-- ###- doctrine/doctrine-bundle ### -->
|
||||||
|
|
||||||
|
<!-- ###+ lexik/jwt-authentication-bundle ### -->
|
||||||
|
<env name="JWT_SECRET_KEY" value="%kernel.project_dir%/config/jwt/private.pem"/>
|
||||||
|
<env name="JWT_PUBLIC_KEY" value="%kernel.project_dir%/config/jwt/public.pem"/>
|
||||||
|
<env name="JWT_PASSPHRASE" value="43d0b1e183b53eb04d458f29a1fa8f19badf1e6f0fd0df587402348a7e7e1e37"/>
|
||||||
|
<!-- ###- lexik/jwt-authentication-bundle ### -->
|
||||||
|
|
||||||
|
<!-- ###+ nelmio/cors-bundle ### -->
|
||||||
|
<env name="CORS_ALLOW_ORIGIN" value="'^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'"/>
|
||||||
|
<!-- ###- nelmio/cors-bundle ### -->
|
||||||
</php>
|
</php>
|
||||||
|
|
||||||
<testsuites>
|
<testsuites>
|
||||||
|
|||||||
0
src/ApiResource/.gitignore
vendored
Normal file
0
src/ApiResource/.gitignore
vendored
Normal file
51
src/ApiResource/ManagedApplication.php
Normal file
51
src/ApiResource/ManagedApplication.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\ApiResource;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use App\State\ManagedApplicationProvider;
|
||||||
|
use App\State\MaintenanceToggleProcessor;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new GetCollection(
|
||||||
|
uriTemplate: '/applications',
|
||||||
|
normalizationContext: ['groups' => ['app:read']],
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
provider: ManagedApplicationProvider::class,
|
||||||
|
),
|
||||||
|
new Get(
|
||||||
|
uriTemplate: '/applications/{slug}',
|
||||||
|
normalizationContext: ['groups' => ['app:read']],
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
provider: ManagedApplicationProvider::class,
|
||||||
|
),
|
||||||
|
new Post(
|
||||||
|
uriTemplate: '/applications/{slug}/maintenance',
|
||||||
|
normalizationContext: ['groups' => ['app:read']],
|
||||||
|
denormalizationContext: ['groups' => ['app:write']],
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
provider: ManagedApplicationProvider::class,
|
||||||
|
processor: MaintenanceToggleProcessor::class,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)]
|
||||||
|
final class ManagedApplication
|
||||||
|
{
|
||||||
|
#[ApiProperty(identifier: true)]
|
||||||
|
#[Groups(['app:read'])]
|
||||||
|
public string $slug = '';
|
||||||
|
|
||||||
|
#[Groups(['app:read'])]
|
||||||
|
public string $name = '';
|
||||||
|
|
||||||
|
#[Groups(['app:read', 'app:write'])]
|
||||||
|
public bool $maintenance = false;
|
||||||
|
}
|
||||||
0
src/Controller/.gitignore
vendored
Normal file
0
src/Controller/.gitignore
vendored
Normal file
0
src/Entity/.gitignore
vendored
Normal file
0
src/Entity/.gitignore
vendored
Normal file
@@ -28,9 +28,11 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
normalizationContext: ['groups' => ['me:read']],
|
normalizationContext: ['groups' => ['me:read']],
|
||||||
),
|
),
|
||||||
new Get(
|
new Get(
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
normalizationContext: ['groups' => ['user:list']],
|
normalizationContext: ['groups' => ['user:list']],
|
||||||
),
|
),
|
||||||
new GetCollection(
|
new GetCollection(
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
normalizationContext: ['groups' => ['user:list']],
|
normalizationContext: ['groups' => ['user:list']],
|
||||||
),
|
),
|
||||||
new Post(security: "is_granted('ROLE_ADMIN')", processor: UserPasswordHasherProcessor::class),
|
new Post(security: "is_granted('ROLE_ADMIN')", processor: UserPasswordHasherProcessor::class),
|
||||||
|
|||||||
0
src/Repository/.gitignore
vendored
Normal file
0
src/Repository/.gitignore
vendored
Normal file
67
src/State/MaintenanceToggleProcessor.php
Normal file
67
src/State/MaintenanceToggleProcessor.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProcessorInterface;
|
||||||
|
use App\ApiResource\ManagedApplication;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
|
final readonly class MaintenanceToggleProcessor implements ProcessorInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param list<array{name: string, slug: string, maintenance_path: string}> $managedApplications
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
#[Autowire('%app.managed_applications%')]
|
||||||
|
private array $managedApplications,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ManagedApplication $data
|
||||||
|
*/
|
||||||
|
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): ManagedApplication
|
||||||
|
{
|
||||||
|
$slug = $uriVariables['slug'] ?? '';
|
||||||
|
$appConfig = null;
|
||||||
|
|
||||||
|
foreach ($this->managedApplications as $app) {
|
||||||
|
if ($app['slug'] === $slug) {
|
||||||
|
$appConfig = $app;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $appConfig) {
|
||||||
|
throw new NotFoundHttpException(sprintf('Application "%s" not found.', $slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
$maintenancePath = $appConfig['maintenance_path'];
|
||||||
|
|
||||||
|
if ($data->maintenance) {
|
||||||
|
$directory = dirname($maintenancePath);
|
||||||
|
|
||||||
|
if (!is_dir($directory) && !mkdir($directory, 0755, true)) {
|
||||||
|
throw new \RuntimeException(sprintf('Cannot create directory "%s".', $directory));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!touch($maintenancePath)) {
|
||||||
|
throw new \RuntimeException(sprintf('Cannot create maintenance file at "%s".', $maintenancePath));
|
||||||
|
}
|
||||||
|
} elseif (file_exists($maintenancePath)) {
|
||||||
|
if (!unlink($maintenancePath)) {
|
||||||
|
throw new \RuntimeException(sprintf('Cannot remove maintenance file at "%s".', $maintenancePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dto = new ManagedApplication();
|
||||||
|
$dto->slug = $appConfig['slug'];
|
||||||
|
$dto->name = $appConfig['name'];
|
||||||
|
$dto->maintenance = file_exists($maintenancePath);
|
||||||
|
|
||||||
|
return $dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/State/ManagedApplicationProvider.php
Normal file
56
src/State/ManagedApplicationProvider.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\ApiResource\ManagedApplication;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
|
final readonly class ManagedApplicationProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param list<array{name: string, slug: string, maintenance_path: string}> $managedApplications
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
#[Autowire('%app.managed_applications%')]
|
||||||
|
private array $managedApplications,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ManagedApplication|array
|
||||||
|
{
|
||||||
|
if ($operation instanceof GetCollection) {
|
||||||
|
return array_map(
|
||||||
|
fn (array $app) => $this->buildDto($app),
|
||||||
|
$this->managedApplications,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$slug = $uriVariables['slug'] ?? '';
|
||||||
|
|
||||||
|
foreach ($this->managedApplications as $app) {
|
||||||
|
if ($app['slug'] === $slug) {
|
||||||
|
return $this->buildDto($app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotFoundHttpException(sprintf('Application "%s" not found.', $slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{name: string, slug: string, maintenance_path: string} $app
|
||||||
|
*/
|
||||||
|
private function buildDto(array $app): ManagedApplication
|
||||||
|
{
|
||||||
|
$dto = new ManagedApplication();
|
||||||
|
$dto->slug = $app['slug'];
|
||||||
|
$dto->name = $app['name'];
|
||||||
|
$dto->maintenance = file_exists($app['maintenance_path']);
|
||||||
|
|
||||||
|
return $dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ use ApiPlatform\Metadata\Operation;
|
|||||||
use ApiPlatform\State\ProviderInterface;
|
use ApiPlatform\State\ProviderInterface;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @implements ProviderInterface<User>
|
* @implements ProviderInterface<User>
|
||||||
@@ -20,7 +21,12 @@ final readonly class MeProvider implements ProviderInterface
|
|||||||
|
|
||||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): User
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): User
|
||||||
{
|
{
|
||||||
// @var User $user
|
$user = $this->security->getUser();
|
||||||
return $this->security->getUser();
|
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
throw new AccessDeniedHttpException('User not authenticated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
243
symfony.lock
Normal file
243
symfony.lock
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
{
|
||||||
|
"api-platform/symfony": {
|
||||||
|
"version": "4.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "4.0",
|
||||||
|
"ref": "e9952e9f393c2d048f10a78f272cd35e807d972b"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/api_platform.yaml",
|
||||||
|
"config/routes/api_platform.yaml",
|
||||||
|
"src/ApiResource/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"doctrine/deprecations": {
|
||||||
|
"version": "1.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.0",
|
||||||
|
"ref": "fdd756167454623e21f1d769c5b814b243782a67"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"doctrine/doctrine-bundle": {
|
||||||
|
"version": "3.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.0",
|
||||||
|
"ref": "d39a3bd844edfe90c20ae520b804a3bf4f82b4ad"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/doctrine.yaml",
|
||||||
|
"src/Entity/.gitignore",
|
||||||
|
"src/Repository/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"doctrine/doctrine-fixtures-bundle": {
|
||||||
|
"version": "4.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.0",
|
||||||
|
"ref": "1f5514cfa15b947298df4d771e694e578d4c204d"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/DataFixtures/AppFixtures.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"doctrine/doctrine-migrations-bundle": {
|
||||||
|
"version": "4.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.1",
|
||||||
|
"ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/doctrine_migrations.yaml",
|
||||||
|
"migrations/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"friendsofphp/php-cs-fixer": {
|
||||||
|
"version": "3.94",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.39",
|
||||||
|
"ref": "97aaf9026490db73b86c23d49e5774bc89d2b232"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".php-cs-fixer.dist.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lexik/jwt-authentication-bundle": {
|
||||||
|
"version": "3.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "2.5",
|
||||||
|
"ref": "e9481b233a11ef7e15fe055a2b21fd3ac1aa2bb7"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/lexik_jwt_authentication.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nelmio/cors-bundle": {
|
||||||
|
"version": "2.6",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.5",
|
||||||
|
"ref": "6bea22e6c564fba3a1391615cada1437d0bde39c"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/nelmio_cors.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nyholm/psr7": {
|
||||||
|
"version": "1.8",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.0",
|
||||||
|
"ref": "4a8c0345442dcca1d8a2c65633dcf0285dd5a5a2"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/nyholm_psr7.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"phpunit/phpunit": {
|
||||||
|
"version": "13.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "11.1",
|
||||||
|
"ref": "ca0bc067abfb40a8de1b2561b96cbfc2b833c314"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".env.test",
|
||||||
|
"phpunit.dist.xml",
|
||||||
|
"tests/bootstrap.php",
|
||||||
|
"bin/phpunit"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/console": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "5.3",
|
||||||
|
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"bin/console"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/flex": {
|
||||||
|
"version": "2.10",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "2.4",
|
||||||
|
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".env",
|
||||||
|
".env.dev"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/framework-bundle": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.4",
|
||||||
|
"ref": "09f6e081c763a206802674ce0cb34a022f0ffc6d"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/cache.yaml",
|
||||||
|
"config/packages/framework.yaml",
|
||||||
|
"config/preload.php",
|
||||||
|
"config/routes/framework.yaml",
|
||||||
|
"config/services.yaml",
|
||||||
|
"public/index.php",
|
||||||
|
"src/Controller/.gitignore",
|
||||||
|
"src/Kernel.php",
|
||||||
|
".editorconfig"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/monolog-bundle": {
|
||||||
|
"version": "4.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.7",
|
||||||
|
"ref": "1b9efb10c54cb51c713a9391c9300ff8bceda459"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/monolog.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/property-info": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.3",
|
||||||
|
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/property_info.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/routing": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.4",
|
||||||
|
"ref": "bc94c4fd86f393f3ab3947c18b830ea343e51ded"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/routing.yaml",
|
||||||
|
"config/routes.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/security-bundle": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.4",
|
||||||
|
"ref": "c42fee7802181cdd50f61b8622715829f5d2335c"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/security.yaml",
|
||||||
|
"config/routes/security.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/uid": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.0",
|
||||||
|
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"symfony/validator": {
|
||||||
|
"version": "8.0",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.0",
|
||||||
|
"ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/validator.yaml"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user