taskori-manage est le portail de gestion interne de Taskori (back-office manager/admin). Il est accessible à l'URL de production https://manage.taskori.app et en environnement dev sur https://manage-dev.taskori.app.
Il s'adresse aux administrateurs et managers d'une entreprise abonnée à Taskori. Son périmètre couvre :
Le projet a été renommé taskori-pay dans son README d'origine (artefact GitLab non mis à jour), mais le nom canonique est taskori-manage.
| Composant | Détail |
|---|---|
| Langage serveur | PHP 8.1 |
| Serveur web | Apache 2 (mod_rewrite) |
| Base de données principale | MariaDB/MySQL — base taskori_manage |
| Base de données auth | MariaDB/MySQL — base taskori_auth (lecture seule via AuthDB) |
| Sessions | Redis 7 (PHP session handler) |
| Envoi d'emails | PHPMailer 6.x via SMTP (Resend) |
| JWT | firebase/php-jwt 6.x — cookie taskori_token HS256 |
| Génération PDF | dompdf 2.x |
| Frontend | Bootstrap 5.3 dark mode, Bootstrap Icons 1.11, Syne + DM Sans (Google Fonts) |
| Internationalisation | PHP natif — fichiers lang/fr.php, lang/en.php, lang/es.php |
| Conteneurisation | Docker — php:8.1-apache + Supervisor |
| CI/CD | GitLab CI — runner tagué taskori-manage |
| Reverse proxy | Traefik (réseau Docker externe web) |
| Stockage fichiers | Délégué à taskori-share via API interne |
L'application est un front-controller classique :
/.htaccess → tout passe par index.php (sauf fichiers physiques)
/index.php → authentification JWT → dispatch vers /pages/{page}.php
/pages/*.php → pages de l'application
/api/*.php → endpoints JSON (support.php)
/api/internal/ → API inter-services (protégées par X-Taskori-Project-Token)
/lib/ → bibliothèques partagées
/config/ → config.php + conf.php.tpl
/lang/ → fichiers de traduction
/sql/ → migrations SQL
Le paramètre ?page=xxx est nettoyé par regex ([^a-z0-9_-]) avant d'être utilisé pour charger le fichier de page correspondant. Les pages inconnues tombent sur dashboard.
require_auth() en premier.require_auth() valide le cookie taskori_token (JWT HS256 signé avec JWT_SECRET, émis par taskori-auth).{TASKORI_AUTH_URL}/login?redirect_uri=....user_uid, email, firstname, lastname, role (admin ou employee), role_uid (pour les rôles custom), company_uid, plan, lang.lib/permissions.php implémente un RBAC à deux niveaux :
write (matrice statique dans _admin_permissions()).taskori-auth via POST {TASKORI_AUTH_URL}/api/permissions (body JSON {role_uid}), mis en cache Redis 300 secondes (clé perms:{role_uid}).La matrice de permissions est organisée par app puis par page :
project → clients, projects, tasks, templates
ts → timesheets, expense_reports
manage → users, expenses, payslips, invoices, roles
Chaque entrée vaut none, read ou write. La fonction can($__perms, $app, $page, $level) est utilisée partout dans les templates pour conditionner l'affichage des sections de la sidebar et l'accès aux actions.
detect_lang() lit dans cet ordre : claim lang du JWT > header Accept-Language > défaut en.fr, en, es.t($key, $vars) remplace les variables {name} dans les chaînes.lib/plan.php centralise la logique de plans :
| Plan | Utilisateurs | Projets actifs | Stockage |
|---|---|---|---|
| solo | 1 | 2 | 10 GB |
| pro | 10 | illimité | 25 GB |
| business | illimité | illimité | 50 GB |
La fonction effective_plan($company) gère les cas de :
active / lifetime → plan de la companytrialing non expiré → solo obtient les features protrialing expiré → downgrade solopast_due → grace period 7 jours, puis solocanceled → plan jusqu'à subscription_ends_at, puis sololimited / inconnu → soloTous les modules (TS, Manage, Project, Share) sont disponibles sur tous les plans ; les plans diffèrent uniquement par les limites quantitatives.
Page (?page=) |
Description | Accès minimum |
|---|---|---|
dashboard |
KPIs (membres, frais en attente, fiches du mois, factures), listes récentes | Tous |
users |
Gestion membres : invite, changer rôle, désactiver/réactiver, import CSV | manage.users.read ou admin |
clients |
CRM : clients, contacts, projets liés, devis, factures projet | manage.clients.read ou admin |
devis-templates |
Templates HTML pour génération de devis PDF via dompdf | Admin |
expenses |
Notes de frais : soumettre (employé), approuver/rejeter (manager), filtrage par statut | manage.expenses.read ou admin |
expense-types |
Catégories de frais (admin/write expenses) | manage.expenses.write ou admin |
payslips |
Upload et consultation fiches de paie PDF + réglages paie (heures semaine) | manage.payslips.read ou admin |
invoices |
Factures entreprise entrantes/sortantes, envoi email | manage.invoices.read ou admin |
project-billing |
Devis et factures liées aux projets (via taskori-project) | Admin |
roles |
Gestion des rôles custom per-company avec matrice de permissions | Admin uniquement |
company |
Paramètres entreprise (nom, timezone, export données, zone danger) | Admin |
billing |
Abonnement Stripe : plan actuel, période d'essai, portail client Stripe | Admin uniquement |
support |
Tickets support via Chatwoot — créer, lister, répondre, clôturer | Tous |
file |
Proxy de téléchargement sécurisé vers taskori-share | Tous (scope company) |
L'intégration Chatwoot remplace un widget de live chat. Les tickets sont des conversations Chatwoot dans l'inbox configurée (CHATWOOT_INBOX_ID). Le client n'a accès qu'à ses propres conversations (sécurité : vérification que la conversation appartient au contact email de l'utilisateur avant tout accès).
api/support.php appelle chatwoot_get_or_create_contact(email, name) — recherche d'abord par email, crée si absent (gestion du 422 en cas de race condition).chatwoot_create_conversation() ouvre la conversation avec status=open, message_type=incoming, et mail_subject dans additional_attributes.[email protected] avec Message-ID: <conv-{id}@taskori.app> pour le threading IMAP.La page support affiche deux onglets :
open + pendingresolved + snoozedUn point orange apparaît dans la sidebar si au moins un ticket a le statut pending (réponse agent en attente de la part du client). Ce badge est calculé côté JS via api/support.php?action=list — il s'affiche aussi sur le dashboard via un fetch silencieux.
| Statut Chatwoot | Label affiché | Couleur |
|---|---|---|
| open | Ouvert | Ambre |
| pending | Réponse attendue | Bleu |
| resolved | Résolu | Vert |
| snoozed | Reporté | Gris |
Quand le client répond à un ticket, le statut repasse à open (via chatwoot_update_conversation_status).
Upload délégué à taskori-share :
upload_attachment envoie le fichier à taskori-share/api/internal/upload.php (base64 JSON, bucket taskori-support, project_uid=ticket_{id}).create_link.php.📎 [nom](url).Chaque action sur une conversation (get_messages, reply, upload_attachment, close_ticket) vérifie que conversation_id appartient bien au contact de l'utilisateur connecté. Un utilisateur ne peut jamais accéder aux tickets d'un autre utilisateur.
lib/StorageService.php est la couche d'abstraction vers taskori-share :
| Méthode | Usage |
|---|---|
store() |
Upload générique (PDF/image, max 10 MB) pour fiches de paie et justificatifs de frais |
storeForClient() |
Upload PDF (max 20 MB) dans dossier client (devis ou factures) |
storePdfContentForClient() |
Upload contenu PDF généré en mémoire (dompdf) dans dossier client |
createClientFolders() |
Crée les dossiers devis et factures lors de la création d'un client |
url() |
URL de téléchargement via le proxy manage (?page=file&uid=...) |
delete() |
Suppression d'un fichier sur share |
getBucketInfo() |
Récupère storage_used et storage_quota pour la jauge de la sidebar |
Toutes les communications avec share passent par TASKORI_SHARE_INTERNAL_URL avec le header X-Taskori-Share-Token.
Ces endpoints sont dans api/internal/ et protégés par le header X-Taskori-Project-Token (valeur = TASKORI_PROJECT_INTERNAL_TOKEN). Ils sont appelés par taskori-project pour enrichir ses données CRM.
| Endpoint | Description |
|---|---|
contact_info.php |
Retourne uid, full_name, email, client_name pour un contact_uid |
contact_info_for_project.php |
Informations de contact enrichies pour un projet donné |
contacts_by_company.php |
Liste tous les contacts d'une company (pour autocompletion) |
projects_by_client.php |
Liste les projets taskori-project associés à un client manage |
lib/mail_functions.php encapsule PHPMailer. La fonction send_mail() sélectionne automatiquement les credentials SMTP selon l'adresse From :
From == SUPPORT_EMAIL, utilise SUPPORT_SMTP_PASS (compte dédié [email protected]).SMTP_USER / SMTP_PASS (compte [email protected]).build_taskori_email() génère un template HTML dark-mode responsive avec header violet dégradé, corps et CTA optionnel.
Fonctions d'envoi spécialisées :
send_devis_email() — envoi d'un devis PDF via lien sharesend_invoice_email() — envoi d'une facture PDF via lien shareDB_NAME)Les migrations sont numérotées sous sql/ :
migration_001 — Frais
expense_types : catégories de frais per-company (uid, name, icon Bootstrap)expenses : notes de frais (uid, company_uid, user_uid, type_id, amount, currency, description, receipt_path, status pending/approved/rejected, submitted_at, reviewed_by_uid, reviewed_at, rejection_reason)migration_002 — Paie et factures
payslips : fiches de paie (uid, company_uid, user_uid, period YYYY-MM, file_path, label, uploaded_by_uid)invoices : factures génériques (uid, company_uid, type incoming/outgoing, label, amount, currency, file_path, email_sent_to, sent_at)migration_003 — CRM
clients : clients de l'entreprise (uid, company_uid, name, notes, soft-delete)client_contacts : contacts avec FK sur clients (first_name, last_name, email, phone, title, is_primary, soft-delete)devis_templates : templates HTML pour génération de devisdevis : devis liés aux projets (template_id, share_file_uid, status draft/sent/accepted/refused)devis_items : lignes d'un devis (description, qty, unit_price, position)project_invoices : factures liées aux projets (status draft/sent/paid)AUTH_DB_NAME)Accès en lecture via AuthDB::get(). Utilisée pour :
user_company_roles + users)roles)entrypoint.sh)| Variable | Description |
|---|---|
DB_HOST |
Hôte MariaDB |
DB_NAME |
Base manage |
DB_USER / DB_PASS |
Credentials base manage |
JWT_SECRET |
Secret HS256 partagé avec taskori-auth |
AUTH_DB_NAME / AUTH_DB_USER / AUTH_DB_PASS |
Credentials lecture base auth |
| Variable | Défaut | Description |
|---|---|---|
APP_URL |
https://manage.taskori.app |
URL publique de l'app |
TASKORI_AUTH_URL |
https://taskori.app |
URL taskori-auth (login, logout, /api/permissions) |
SENTRY_DSN |
(vide) | DSN Sentry si monitoring d'erreurs |
SMTP_HOST |
smtp.resend.com |
Serveur SMTP sortant |
SMTP_PORT |
587 |
Port SMTP |
SMTP_USER |
resend |
Utilisateur SMTP noreply |
SMTP_PASS |
(vide) | Mot de passe SMTP noreply |
SMTP_SECURE |
tls |
Type TLS |
SMTP_FROM |
[email protected] |
Adresse From par défaut |
SMTP_FROMNAME |
Taskori |
Nom From par défaut |
SUPPORT_EMAIL |
[email protected] |
Adresse From pour les emails support |
SUPPORT_NAME |
Taskori Support |
Nom From pour les emails support |
SUPPORT_SMTP_PASS |
(obligatoire si support Chatwoot) | Mot de passe compte support |
TASKORI_SHARE_INTERNAL_URL |
http://taskori-share-web |
URL interne taskori-share |
TASKORI_SHARE_TOKEN |
(obligatoire) | Token X-Taskori-Share-Token |
TASKORI_ADMIN_URL |
http://taskori-admin-web |
URL interne taskori-admin |
TASKORI_ADMIN_INTERNAL_TOKEN |
(obligatoire) | Token API admin (Stripe) |
TASKORI_PROJECT_INTERNAL_URL |
http://taskori-project-web |
URL interne taskori-project |
TASKORI_PROJECT_INTERNAL_TOKEN |
(obligatoire) | Token API project |
TASKORI_TS_INTERNAL_URL |
http://taskori-ts-web |
URL interne taskori-ts |
TASKORI_TS_INTERNAL_TOKEN |
(obligatoire) | Token API ts |
CHATWOOT_URL |
(vide) | URL Chatwoot |
CHATWOOT_API_TOKEN |
(vide) | Token API Chatwoot (api_access_token) |
CHATWOOT_ACCOUNT_ID |
(vide) | ID compte Chatwoot |
CHATWOOT_INBOX_ID |
(vide) | ID inbox Chatwoot (inbox #2 = support Taskori) |
REDIS_HOST |
redis |
Hôte Redis (sessions + cache permissions) |
REDIS_PORT |
6379 |
Port Redis |
Si CHATWOOT_* est absent ou incomplet, la page Support affiche un message d'information et tous les appels à chatwoot_is_configured() retournent false silencieusement.
Le pipeline GitLab (.gitlab-ci.yml) utilise le runner tagué taskori-manage sur la VM de prod LORVA.
| Stage | Branch | Description |
|---|---|---|
test (lint) |
toutes | php -l sur tous les .php hors vendor |
build-dev |
dev |
Build image Docker, push vers registry.lorva.dev avec tag dev |
deploy-dev |
dev |
Pull + docker compose -f docker-compose.dev.yml up -d |
build |
main |
Build image Docker, push avec tag {slug} et latest |
deploy |
main |
Génération .env depuis variables CI, scp vers /docker/taskori-manage/, SSH + pull + up |
.env en déploiementLe job deploy filtre les variables CI dont le nom commence par l'un des préfixes suivants et les injecte dans un .env local avant scp :
DB_ AUTH_ MANAGE_DB_ TS_DB_ SHARE_DB_ PROJECT_DB_ JWT_
MASTER_ APP_URL SMTP_ TASKORI_ CHATWOOT_ ENCRYPTION_ INTERNAL_ SENTRY_ SUPPORT_
DEPLOY_HOST — IP ou hostname de la VM de déploiementDEPLOY_USER — utilisateur SSH de déploiementdocker-compose.yml)registry.lorva.dev/taskori/taskori-manage:latesttaskori-manage-web/opt/taskori-manage/logs → /var/log/apache2/opt/taskori-manage/uploads → /opt/taskori-manage/uploadsweb (IP fixe 172.32.0.182) pour Traefikdb (IP fixe 172.34.0.182) pour accès MariaDBtaskori-manage-network pour Redistaskori-manage-redis) sur le réseau internerateLimit TraefikHost(manage.taskori.app) sur entrypoint webwud.watch=false — pas de mise à jour automatique des imagesdocker-compose.dev.yml)registry.lorva.dev/taskori/taskori-manage:devtaskori-manage-dev-webmanage-dev.taskori.appwebsecure avec TLS Let's Encrypt (DNS-01)php:8.1-apache
├── Extensions PHP : pdo, pdo_mysql, gmp, zip, gd, curl, redis (PECL)
├── Composer (install au build)
├── Apache : mod_rewrite, AllowOverride All
├── PHP ini : upload_max_filesize=20M, post_max_size=22M, display_errors=Off
├── Supervisor : gère le processus Apache
└── entrypoint.sh :
├── Validation des variables obligatoires
├── envsubst conf.php.tpl → conf.php
└── Configuration session Redis (/usr/local/etc/php/conf.d/99-redis-sessions.ini)
La configuration PHP est générée dynamiquement par envsubst à partir du template config/conf.php.tpl. Aucune valeur sensible n'est baked dans l'image.
htmlspecialchars() systématique sur toutes les sorties.conversation_id appartient aux conversations du contact email de l'utilisateur.index.php et renforcés au niveau Traefik.page est sanitisé par regex avant utilisation dans file_exists().conf.php jamais committéLe fichier config/conf.php est généré à l'exécution par entrypoint.sh via envsubst. Il n'existe pas en dehors du conteneur. Seul conf.php.tpl est versionné. Ne jamais créer conf.php manuellement et l'ajouter au repo.
mail_functions.php bascule automatiquement les credentials SMTP selon l'adresse From. Pour que les emails de support partent depuis [email protected], la variable SUPPORT_SMTP_PASS doit être renseignée. Si elle est absente, le fallback SMTP_PASS sera utilisé mais l'adresse From restera [email protected].
Si Redis est indisponible au moment du calcul des permissions, get_permissions() fait directement l'appel HTTP à taskori-auth sans cache. L'app reste fonctionnelle mais chaque page fera un appel réseau supplémentaire.
L'app ouvre deux connexions PDO : la principale vers DB_NAME (données manage) et une secondaire via AuthDB::get() vers AUTH_DB_NAME (singleton). Les deux utilisent le même DB_HOST. Les credentials sont distincts (DB_USER/PASS vs AUTH_DB_USER/PASS).
L'CHATWOOT_INBOX_ID correspond à l'inbox #2 de Chatwoot (inbox dédiée au support Taskori depuis l'intégration email-to-ticket). Changer cet ID sans vérification déroute les tickets vers la mauvaise inbox.
Depuis le commit bf5096d, le rate limiter Traefik utilise ipStrategy.depth=1 pour extraire l'IP réelle derrière Cloudflare (X-Forwarded-For). En prod, s'assurer que Traefik a trustedIPs configuré pour les plages Cloudflare dans traefik.yml (config globale, pas les labels docker-compose).
api/internal/Les endpoints api/internal/*.php vérifient TASKORI_PROJECT_INTERNAL_TOKEN via hash_equals() (protection timing attack). Si ce token change dans taskori-manage, il doit être mis à jour dans taskori-project en même temps.
Les clients et contacts ne sont jamais supprimés physiquement de la base : deleted_at IS NULL filtre les entrées actives dans toutes les requêtes. Les suppressions via l'interface posent deleted_at = NOW().
Le déploiement s'effectue via le runner GitLab tagué taskori-manage qui tourne directement sur la VM de prod. La clé SSH est montée depuis le filesystem du runner, pas injectée comme variable CI. Ne pas modifier ce mécanisme sans mettre à jour la config du runner.
Un worktree feature/crm existe dans .worktrees/feature/crm/ — branche de développement CRM déjà mergée dans main (migration_003). Le worktree peut être nettoyé.
taskori-manage
├── taskori-auth → validation JWT, API permissions (/api/permissions)
├── taskori-share → upload/download fichiers, bucket info, dossiers clients
├── taskori-admin → API Stripe (portail client, infos abonnement)
├── taskori-project → données projets liés aux clients (via internal API bidirectionnelle)
├── taskori-ts → (token configuré, appels futurs)
└── Chatwoot → tickets support (inbox #2)
Les communications inter-services utilisent les URLs internes Docker (http://taskori-*-web) avec des tokens Bearer/header dédiés (X-Internal-Token, X-Taskori-Share-Token, X-Taskori-Project-Token).