# Lesstime Application de gestion de projet avec suivi du temps et portail client. ## Stack | Couche | Technologies | |--------|-------------| | **Backend** | PHP 8.4, Symfony 8, API Platform 4, Doctrine ORM | | **Frontend** | Nuxt 4 (SPA), Vue 3, Pinia, Tailwind CSS | | **Base de données** | PostgreSQL 16 | | **Auth** | JWT HTTP-only cookie (lexik/jwt-authentication-bundle) | | **Infrastructure** | Docker (PHP-FPM, Nginx, PostgreSQL) | ## Fonctionnalités - Gestion de projets et tâches (kanban, groupes, priorités, tags, efforts) - Suivi du temps (timer, calendrier, vue liste) - Portail client avec tickets (bug, amélioration, autre) - Gestion de documents (upload, prévisualisation, téléchargement) - Profil utilisateur avec avatar (crop circulaire) - Notifications temps réel - Intégration Gitea (issues, repos) - Intégration Mail IMAP (boîte partagée OVH, voir `docs/mail-integration.md`) - Serveur MCP pour assistants IA - Error tracking centralisé back + front (GlitchTip / SDK Sentry, prod uniquement — voir « Error tracking ») - Multi-langue (i18n) ## Prérequis - Docker & Docker Compose - Git ## Installation ```bash # 1. Cloner le repo git clone && cd lesstime # 2. Démarrer les containers make start # 3. Installation complète (composer, migrations, fixtures, build Nuxt) make install ``` L'application est accessible sur **http://localhost:8082**. Les valeurs par défaut du `.env` committé suffisent pour démarrer en local. Pour la prod (et pour activer la messagerie), surcharger les variables sensibles dans `.env.local` — voir « Variables d'environnement » ci-dessous. ### Comptes de test (fixtures) | Utilisateur | Mot de passe | Rôle | Détails | |-------------|-------------|------|---------| | `admin` | `admin` | ROLE_ADMIN | Administrateur | | `alice` | `alice` | ROLE_USER | Utilisateur interne | | `bob` | `bob` | ROLE_USER | Utilisateur interne | | `charlie` | `charlie` | ROLE_USER | Utilisateur interne | | `client-liot` | `client` | ROLE_CLIENT | Client LIOT (projet SIRH) | | `client-acme` | `client` | ROLE_CLIENT | Client ACME (projet CRM) | ## Variables d'environnement Les variables sont définies dans `.env` (committé, valeurs par défaut pour le dev) et peuvent être surchargées dans `.env.local` (jamais committé). En prod, elles vont dans le `.env` du serveur (`/var/www/lesstime/.env`, voir `infra/prod/.env.example`). | Variable | Rôle | Défaut dev | À fixer en prod | |----------|------|-----------|-----------------| | `APP_SECRET` | Secret Symfony | placeholder | ✅ (hex 32) | | `JWT_PASSPHRASE` | Passphrase des clés JWT | placeholder | ✅ | | `DATABASE_URL` | Connexion PostgreSQL | container `db` | ✅ (`host.docker.internal`) | | `CORS_ALLOW_ORIGIN` | Origines CORS autorisées | localhost | ✅ (domaine prod) | | **`ENCRYPTION_KEY`** | **Clé hex 32 bytes chiffrant les credentials IMAP/SMTP (feature mail)** | placeholder | ✅ — doit rester **stable**, sinon les credentials mail stockés deviennent illisibles | | **`LOCK_DSN`** | **Store de verrous Symfony pour la sync mail (anti-chevauchement)** | `flock` | `flock` suffit | | `SENTRY_DSN` | Error tracking **backend** → GlitchTip (projet `lesstime-api`) | _(vide)_ | ⚪ optionnel — active le tracking (voir « Error tracking ») | > **Messagerie** : `ENCRYPTION_KEY` et `LOCK_DSN` sont introduites par l'intégration mail. > Détails de config et cron de synchronisation : `docs/mail-integration.md` et `docs/mail-cron-setup.md`. > Générer une clé : `php -r "echo bin2hex(random_bytes(32));"`. ## Commandes ### Docker ```bash make start # Démarrer les containers make stop # Arrêter les containers make restart # Redémarrer les containers make shell # Shell dans le container PHP make shell-root # Shell root dans le container PHP ``` ### Développement ```bash make dev-nuxt # Dev server Nuxt (hot reload, port 3002) make cache-clear # Vider le cache Symfony make logs-dev # Tail logs Symfony make mail-sync # Synchroniser la boîte mail IMAP (voir docs/mail-cron-setup.md) ``` ### Base de données ```bash make migration-migrate # Lancer les migrations make fixtures # Charger les fixtures make db-reset # Reset BDD + migrations + fixtures (⚠️ supprime les données) ``` ### Tests & Qualité ```bash make test # PHPUnit make php-cs-fixer-allow-risky # Fix code style PHP (Symfony + PSR-12) ``` ### Installation complète ```bash make install # Composer + migrations + fixtures + build Nuxt make reset # Tout supprimer et réinstaller (⚠️ supprime la BDD) ``` ## Architecture ``` src/ ├── Entity/ # Entités Doctrine ├── ApiResource/ # Ressources API Platform (découplées) ├── State/ # Providers et Processors API Platform ├── Controller/ # Controllers custom Symfony ├── Service/ # Services métier ├── EventListener/ # Listeners Doctrine ├── Exception/ # Exceptions custom ├── Security/ # Authenticators custom ├── Repository/ # Repositories Doctrine ├── Command/ # Commandes console ├── DataFixtures/ # Fixtures └── Mcp/Tool/ # MCP tools par domaine ├── Project/ ├── Task/ ├── TaskMeta/ ├── TimeEntry/ └── Reference/ frontend/ ├── pages/ # Pages Nuxt (routing auto) │ ├── portal/ # Pages portail client │ └── projects/ # Pages projets ├── layouts/ # Layouts (default, portal) ├── components/ # Composants Vue │ ├── ui/ # Composants génériques │ ├── task/ # Tâches │ ├── user/ # Utilisateur (avatar, etc.) │ ├── project/ # Projets │ ├── client/ # Clients │ ├── client-ticket/ # Tickets client │ ├── admin/ # Administration │ ├── notification/ # Notifications │ └── time-tracking/ # Suivi du temps ├── composables/ # Composables (useApi, useNotifications, etc.) ├── stores/ # Stores Pinia (auth, ui, timer) ├── services/ # Services API │ └── dto/ # Types TypeScript ├── plugins/ # Plugins Nuxt ├── utils/ # Utilitaires ├── i18n/locales/ # Traductions └── middleware/ # Middleware auth config/ # Config Symfony migrations/ # Migrations Doctrine docker/ # Dockerfiles et config Nginx ``` ## Docker | Container | Port | Description | |-----------|------|-------------| | `php-lesstime-fpm` | 3002 (dev Nuxt) | PHP-FPM + Node 24 | | `nginx-lesstime` | 8082 | Nginx reverse proxy | | PostgreSQL | 5435 | Base de données | Configuration : `infra/dev/.env.docker` (override local : `infra/dev/.env.docker.local`) ## API Toutes les routes API sont préfixées `/api` (API Platform). - Documentation auto-générée : **http://localhost:8082/api** - Auth : `POST /login_check` avec `{ username, password }` → cookie JWT `BEARER` ## Serveur MCP Lesstime expose un serveur MCP (Model Context Protocol) permettant aux assistants IA d'interagir avec les données. ### Tools disponibles (22) | Domaine | Tools | |---------|-------| | Reference | `list-users`, `list-clients` | | Project | `list-projects`, `get-project`, `create-project`, `update-project` | | Task | `list-tasks`, `get-task`, `create-task`, `update-task`, `delete-task` | | TaskMeta | `list-statuses`, `list-priorities`, `list-efforts`, `list-tags`, `list-groups`, `create-group`, `update-group` | | TimeEntry | `list-time-entries`, `create-time-entry`, `update-time-entry`, `delete-time-entry` | ### Configuration locale (STDIO) ```json { "mcpServers": { "lesstime": { "command": "docker", "args": ["exec", "-i", "php-lesstime-fpm", "php", "bin/console", "mcp:server"] } } } ``` ### Configuration réseau (HTTP) ```json { "mcpServers": { "lesstime": { "type": "url", "url": "http://:8082/_mcp", "headers": { "Authorization": "Bearer " } } } } ``` ### Gestion des tokens API ```bash docker exec -u www-data php-lesstime-fpm php bin/console app:generate-api-token ``` ## Déploiement La prod tourne en **Docker** : l'image est buildée par la CI Gitea sur push de tag `v*` (`gitea.malio.fr/malio-dev/lesstime:`), puis déployée par le script `deploy.sh` sur le serveur (dossier `/var/www/lesstime`, container `lesstime-app`). ```bash # Sur le serveur, depuis /var/www/lesstime sudo ./deploy.sh # déploie la dernière image (latest) sudo ./deploy.sh v0.4.2 # déploie une version précise ``` Le script active la maintenance, pull l'image, redémarre le container, lance les migrations et vide le cache. Guide complet (première installation, BDD, Nginx, JWT, rollback) : **`doc/deployment-docker.md`**. ## Error tracking (GlitchTip) Les erreurs **backend** et **frontend** sont remontées vers **GlitchTip** (instance auto-hébergée interne, compatible SDK Sentry) qui les **groupe par projet** et compte les occurrences. Activé **uniquement en prod** : en dev, sans DSN, le SDK est inerte (zéro impact). Ticket de référence : INFRA #146. ### Pourquoi back et front se configurent différemment | | Backend (Symfony) | Frontend (Nuxt SPA) | |---|---|---| | Nature | process PHP qui tourne en continu | fichiers JS/HTML **statiques** (`nuxt generate`) | | Quand le DSN est lu | au **runtime** | **figé au build** (baké dans le JS) | | Où mettre le DSN | `infra/prod/.env` (runtime) | **secrets Gitea** → build-args de la CI | > Les erreurs partent **toujours vers GlitchTip**, jamais vers la CI. La CI ne sert qu'à *écrire* > le DSN front dans le bundle au moment du build (il n'y a aucun process front en prod qui > pourrait lire une variable d'environnement). ### Variables **Backend — fichier `infra/prod/.env` du serveur** (chargé via `env_file`) : ```env SENTRY_DSN=http://@glitchtip.interne:/ ``` **Frontend — secrets Gitea** (repo → Settings → Actions → Secrets), consommés par `.gitea/workflows/build-docker.yml` : | Secret Gitea | Rôle | |---|---| | `SENTRY_FRONT_DSN` | DSN du projet `lesstime-front` (public, baké dans le JS) | | `SENTRY_URL` | URL de l'instance GlitchTip | | `SENTRY_ORG` | slug de l'organisation GlitchTip | | `SENTRY_FRONT_PROJECT` | slug du projet front | | `SENTRY_AUTH_TOKEN` | token d'upload des **source maps** (vrai secret) | > Sans source maps, seul `SENTRY_FRONT_DSN` est requis (les stacktraces front seront sur du JS > minifié). Le build n'échoue pas si les autres secrets sont absents. ### Fichiers concernés | Fichier | Rôle | |---|---| | `config/packages/sentry.yaml` | conf backend (prod-only, exceptions, 4xx ignorés, release = `app.version`) | | `config/bundles.php` | `SentryBundle` enregistré `['prod' => true]` | | `frontend/nuxt.config.ts` | module Sentry chargé **uniquement si DSN présent** + upload source maps | | `frontend/sentry.client.config.ts` | init du SDK client (no-op si DSN vide) | | `infra/prod/Dockerfile` | build-args front (`NUXT_PUBLIC_SENTRY_DSN`, `SENTRY_*`) | | `.gitea/workflows/build-docker.yml` | injection des secrets Gitea en build-args | ### Activation (résumé) 1. Dans GlitchTip : créer les projets `lesstime-api` et `lesstime-front`, récupérer les 2 DSN (+ un auth token pour les source maps). 2. Backend : ajouter `SENTRY_DSN` dans `infra/prod/.env` du serveur. 3. Frontend : ajouter les secrets Gitea ci-dessus. 4. Tagger une version (`v*`) → la CI build l'image avec le DSN front baké → `deploy.sh`. ### Certificat HTTPS interne (CA auto-signée) GlitchTip est servi en **HTTPS** sur `https://logs.malio-dev.fr` (nginx devant), avec un certificat **auto-signé** par une **CA interne** (« MALIO-DEV Local Root CA », cert serveur `*.malio-dev.fr`). `malio-dev.fr` est un **domaine interne uniquement** (DNS local, pas de résolution publique). > **Pourquoi pas Let's Encrypt ?** Une CA publique doit valider le domaine via Internet (challenge > HTTP ou DNS public). Comme `malio-dev.fr` n'existe qu'en interne, aucune validation n'est > possible → on reste sur la CA interne, qu'il faut faire **approuver partout** où la connexion TLS > est établie. Tant que la CA n'est pas approuvée, **rien ne remonte** : le backend logue > « Message not sent » (SDK Sentry) et le navigateur affiche « connexion non sécurisée » (le front > n'envoie rien). **Qui doit faire confiance à la CA ?** La connexion à `logs.malio-dev.fr` part de deux endroits différents, donc deux fixes distincts : | Émetteur des erreurs | Qui établit le TLS | Où approuver la CA | |---|---|---| | Backend (Symfony) | le **container PHP** | CA bakée dans l'**image Docker** (ci-dessous) | | Frontend (SPA) | le **navigateur du poste** | CA poussée sur les **postes via GPO** (ci-dessous) | #### Fix backend — CA bakée dans l'image Le certificat **public** de la root CA est committé dans le repo (`infra/prod/malio-dev-root-ca.crt`, aucune clé privée) et installé dans le trust store du container au build (`infra/prod/Dockerfile`, stage production — `ca-certificates` est déjà installé) : ```dockerfile COPY infra/prod/malio-dev-root-ca.crt /usr/local/share/ca-certificates/malio-dev-root-ca.crt RUN update-ca-certificates ``` Le container fait alors confiance à tout `*.malio-dev.fr` interne et le SDK Sentry backend peut envoyer. Vérification : ```bash curl --cacert infra/prod/malio-dev-root-ca.crt https://logs.malio-dev.fr/api/1/store/ # → HTTP 200 ``` #### Fix postes — CA poussée par GPO (Active Directory) Le front est une SPA : c'est le **navigateur de l'utilisateur** qui contacte `logs.malio-dev.fr`, donc c'est le **poste** qui doit faire confiance à la CA (la CA de l'image ne sert qu'au backend). Sur le domaine Active Directory, on pousse la CA **une seule fois via GPO** plutôt que poste par poste : 1. Contrôleur de domaine → **Group Policy Management** → éditer une GPO. 2. `Configuration ordinateur → Stratégies → Paramètres Windows → Paramètres de sécurité → Stratégies de clé publique → Autorités de certification racines de confiance`. 3. Clic droit → **Importer** → sélectionner `rootCA.crt` (« MALIO-DEV Local Root CA »). 4. Sur les postes : `gpupdate /force` (ou attendre le rafraîchissement), puis **redémarrer le navigateur**. - Chrome / Edge utilisent le magasin Windows → confiance automatique. - ⚠️ **Firefox** a son propre magasin : activer `security.enterprise_roots.enabled = true` (`about:config` ou via policy) pour qu'il lise le magasin Windows. > **Validation poste** : ouvrir `https://logs.malio-dev.fr` → cadenas vert sans avertissement = CA > approuvée = le front peut envoyer. #### Renouvellement / changement de CA Si la CA interne change (rotation, expiration) : 1. Remplacer `infra/prod/malio-dev-root-ca.crt` par le nouveau certificat public, commit + **rebuild de l'image** (re-tag `v*`) pour le backend. 2. **Re-pousser** la nouvelle CA via GPO (étapes ci-dessus) pour les postes. ## Licence Propriétaire — Tous droits réservés.