14 Commits

Author SHA1 Message Date
gitea-actions
44e1e4a293 chore: bump version to v0.1.5
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s
Build & Push Docker Image / build (push) Successful in 18s
2026-04-03 11:12:34 +00:00
ad92a4c434 feat : commande app:create-user pour créer des utilisateurs
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Permet de créer un user en prod sans SQL :
  php bin/console app:create-user <username> <password> --admin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:12:28 +02:00
gitea-actions
be12175e17 chore: bump version to v0.1.4
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s
Build & Push Docker Image / build (push) Successful in 35s
2026-04-03 11:09:26 +00:00
e8fc85c173 fix : correctifs de sécurité et robustesse post-review
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
- MeProvider : guard null user avec AccessDeniedHttpException
- MaintenanceToggleProcessor : vérification des opérations filesystem
- User : restreindre Get/GetCollection aux ROLE_ADMIN
- useAppVersion : corriger le path relatif '/version'

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:09:14 +02:00
gitea-actions
b39e6f81d8 chore: bump version to v0.1.3
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s
Build & Push Docker Image / build (push) Successful in 46s
2026-04-03 10:46:11 +00:00
28690be509 revert(build) : retire le contournement ipv4 du dockerfile
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-04-03 12:46:00 +02:00
gitea-actions
7f6634bec7 chore: bump version to v0.1.2
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
Build & Push Docker Image / build (push) Successful in 55s
2026-04-03 10:08:44 +00:00
b0b05970c1 build(central) : force ipv4 pour composer et npm
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-04-03 12:08:33 +02:00
gitea-actions
57be0bbf85 chore: bump version to v0.1.1
Some checks failed
Auto Tag Develop / tag (push) Successful in 5s
Build & Push Docker Image / build (push) Failing after 1m33s
2026-04-03 09:53:09 +00:00
d85d1cc1d6 fix(frontend) : regenere le package-lock
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
2026-04-03 11:52:57 +02:00
7a3e010e88 ci(central) : utilise registry token pour l auto-tag
Some checks failed
Auto Tag Develop / tag (push) Successful in 6s
Build & Push Docker Image / build (push) Failing after 2m59s
2026-04-03 11:42:59 +02:00
9529f4cf63 chore(central) : aligne la doc prod et l auto-tag
Some checks failed
Auto Tag Develop / tag (push) Failing after 4s
2026-04-03 11:41:46 +02:00
30038255da ci(central) : ajoute les workflows gitea de build et tag 2026-04-03 11:38:04 +02:00
2844fea802 chore(central) : passe le port http a 8084 2026-04-03 11:37:04 +02:00
15 changed files with 226 additions and 30 deletions

View 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"

View 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

View File

@@ -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

View File

@@ -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`

View File

@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '0.1.0' app.version: '0.1.5'

View File

@@ -81,7 +81,7 @@ services:
container_name: central-app container_name: central-app
env_file: .env env_file: .env
ports: ports:
- "8083:80" - "8084:80"
volumes: volumes:
- ./config/jwt:/var/www/html/config/jwt:ro - ./config/jwt:/var/www/html/config/jwt:ro
- ./uploads:/var/www/html/var/uploads - ./uploads:/var/www/html/var/uploads
@@ -112,15 +112,15 @@ APP_SECRET=<generer avec: openssl rand -hex 32>
DATABASE_URL="postgresql://malio:motdepasse@host.docker.internal:5432/central_prod?serverVersion=16&charset=utf8" DATABASE_URL="postgresql://malio:motdepasse@host.docker.internal:5432/central_prod?serverVersion=16&charset=utf8"
DEFAULT_URI=https://central.malio-dev.fr DEFAULT_URI=http://central.malio-dev.fr
APP_SHARE_DIR=var/share APP_SHARE_DIR=var/share
CORS_ALLOW_ORIGIN='^https?://central\.malio-dev\.fr$' CORS_ALLOW_ORIGIN='^http://central\.malio-dev\.fr$'
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=<generer avec: openssl rand -hex 32> JWT_PASSPHRASE=<generer avec: openssl rand -hex 32>
JWT_COOKIE_SECURE=1 JWT_COOKIE_SECURE=0
JWT_TOKEN_TTL=86400 JWT_TOKEN_TTL=86400
JWT_COOKIE_TTL=86400 JWT_COOKIE_TTL=86400
@@ -171,7 +171,7 @@ server {
server_name central.malio-dev.fr; server_name central.malio-dev.fr;
location / { location / {
proxy_pass http://127.0.0.1:8083; proxy_pass http://127.0.0.1:8084;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -212,7 +212,7 @@ cd /var/www/central
## Verification apres deploiement ## Verification apres deploiement
1. Ouvrir `https://central.malio-dev.fr` 1. Ouvrir `http://central.malio-dev.fr`
2. Se connecter avec un compte admin 2. Se connecter avec un compte admin
3. Verifier que la page Applications charge 3. Verifier que la page Applications charge
4. Activer la maintenance sur SIRH 4. Activer la maintenance sur SIRH

View File

@@ -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

View File

@@ -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

View File

@@ -570,6 +570,29 @@
"integrity": "sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==", "integrity": "sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": { "node_modules/@emnapi/wasi-threads": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
@@ -5692,6 +5715,16 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16"
}
},
"node_modules/commondir": { "node_modules/commondir": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -12650,15 +12683,6 @@
"url": "https://opencollective.com/svgo" "url": "https://opencollective.com/svgo"
} }
}, },
"node_modules/svgo/node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/tagged-tag": { "node_modules/tagged-tag": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
@@ -12764,7 +12788,6 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",

View File

@@ -4,7 +4,7 @@ services:
container_name: central-app container_name: central-app
env_file: .env env_file: .env
ports: ports:
- "8083:80" - "8084:80"
volumes: volumes:
- ./config/jwt:/var/www/html/config/jwt:ro - ./config/jwt:/var/www/html/config/jwt:ro
- ./uploads:/var/www/html/var/uploads - ./uploads:/var/www/html/var/uploads

View File

@@ -4,7 +4,7 @@ server {
server_name central.malio-dev.fr; server_name central.malio-dev.fr;
location / { location / {
proxy_pass http://127.0.0.1:8083; proxy_pass http://127.0.0.1:8084;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
#[AsCommand(
name: 'app:create-user',
description: 'Create a new user',
)]
final class CreateUserCommand extends Command
{
public function __construct(
private readonly EntityManagerInterface $em,
private readonly UserPasswordHasherInterface $passwordHasher,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('username', InputArgument::REQUIRED, 'Username')
->addArgument('password', InputArgument::REQUIRED, 'Plain text password')
->addOption('admin', 'a', InputOption::VALUE_NONE, 'Grant ROLE_ADMIN');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$username = $input->getArgument('username');
$plainPassword = $input->getArgument('password');
$existing = $this->em->getRepository(User::class)->findOneBy(['username' => $username]);
if ($existing) {
$io->error(sprintf('User "%s" already exists.', $username));
return Command::FAILURE;
}
$user = new User();
$user->setUsername($username);
$user->setPassword($this->passwordHasher->hashPassword($user, $plainPassword));
$user->setRoles($input->getOption('admin') ? ['ROLE_ADMIN'] : []);
$this->em->persist($user);
$this->em->flush();
$io->success(sprintf('User "%s" created%s.', $username, $input->getOption('admin') ? ' (admin)' : ''));
return Command::SUCCESS;
}
}

View 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),

View File

@@ -44,13 +44,17 @@ final readonly class MaintenanceToggleProcessor implements ProcessorInterface
if ($data->maintenance) { if ($data->maintenance) {
$directory = dirname($maintenancePath); $directory = dirname($maintenancePath);
if (!is_dir($directory)) { if (!is_dir($directory) && !mkdir($directory, 0755, true)) {
mkdir($directory, 0755, true); throw new \RuntimeException(sprintf('Cannot create directory "%s".', $directory));
} }
touch($maintenancePath); if (!touch($maintenancePath)) {
throw new \RuntimeException(sprintf('Cannot create maintenance file at "%s".', $maintenancePath));
}
} elseif (file_exists($maintenancePath)) { } elseif (file_exists($maintenancePath)) {
unlink($maintenancePath); if (!unlink($maintenancePath)) {
throw new \RuntimeException(sprintf('Cannot remove maintenance file at "%s".', $maintenancePath));
}
} }
$dto = new ManagedApplication(); $dto = new ManagedApplication();

View File

@@ -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;
} }
} }