taskori-share est le service de partage de fichiers de Taskori. Il expose une API REST v1 pour les clients authentifiés (JWT), une API interne inter-services protégée par token fixe, et une interface web publique pour les destinataires des liens de partage.
https://share.taskori.appregistry.lorva.dev/taskori/taskori-sharetaskori-share/mnt/NFS-data/taskori/share (dans le conteneur : /opt/taskori-share/uploads) ┌─────────────────────────────┐
Utilisateur Taskori ────► /api/v1/* (JWT Bearer/Cookie)│
│ │
Client externe ────────► /s/{token} (lien de partage) │
│ │
Services internes ──────► /api/internal/* (X-Taskori-Share-Token)
└─────────────────────────────┘
│
┌──────────┴──────────┐
taskori_share DB taskori_project DB
(buckets, files, (clients, projects —
folders, links) lecture seule)
Le service gère deux bases de données distinctes :
taskori_share : données propres au service (buckets, fichiers, dossiers, liens)taskori_project (accès lecture) : base du service manage, pour résoudre les client_uid et project_uid en noms lisiblesNote importante : Le chiffrement est côté serveur, pas E2E. C'est intentionnel : le serveur doit pouvoir déchiffrer les fichiers pour les servir aux destinataires des liens publics. L'objectif est la protection des fichiers au repos sur le NFS, pas la confidentialité vis-à-vis du service lui-même.
MASTER_KEY (env, 64 hex = 32 bytes)
│
├─ chiffre → bucket_key (32 bytes aléatoires par company)
│ stockée chiffrée en DB (table buckets.encryption_key)
│
└─ bucket_key chiffre chaque fichier
Chaque fichier .enc sur le NFS :
[ nonce 12 bytes ][ GCM tag 16 bytes ][ ciphertext ]
base64( nonce(12) || tag(16) || encrypted_bucket_key(32) )
lib/crypto.php)| Méthode | Rôle |
|---|---|
Crypto::generateKey() |
Génère 32 bytes aléatoires (clé bucket) |
Crypto::encryptBucketKey($key) |
Chiffre la clé bucket avec MASTER_KEY → base64 pour la DB |
Crypto::decryptBucketKey($enc) |
Inverse : récupère la clé bucket en clair |
Crypto::encryptFile($data, $key) |
Chiffre le contenu brut d'un fichier avec la clé bucket |
Crypto::decryptFile($data, $key) |
Déchiffre un fichier .enc |
{UPLOAD_PATH}/{company_uid}/{client_uid|_root}/{project_uid|_root}/{uid}.enc
lib/bucket.php)Un bucket = un espace de stockage par company. Créé automatiquement à la première utilisation (getOrCreate).
| Plan | Quota |
|---|---|
solo |
10 GB |
pro |
25 GB |
business |
50 GB |
Si le plan change, le quota est mis à jour automatiquement au prochain appel getOrCreate. La clé de chiffrement reste inchangée.
| Méthode | Rôle |
|---|---|
Bucket::getOrCreate($company_uid, $plan) |
Récupère ou crée le bucket ; met à jour le quota si plan changé |
Bucket::hasQuota($bucket, $fileSize) |
Vérifie si storage_used + fileSize <= storage_quota |
Bucket::addUsage($bucketId, $bytes) |
Incrémente storage_used après upload |
Bucket::removeUsage($bucketId, $bytes) |
Décrémente (GREATEST(0, ...)) après suppression |
Bucket::getKey($bucket) |
Déchiffre et retourne la clé bucket en clair |
Bucket::formatBytes($bytes) |
Formatage lisible (B/KB/MB/GB) |
Les requêtes API utilisent Authorization: Bearer <jwt>. Apache strippant parfois cet en-tête, auth_middleware.php utilise une triple fallback :
$_SERVER['HTTP_AUTHORIZATION']$_SERVER['REDIRECT_HTTP_AUTHORIZATION'] (via .htaccess RewriteRule)apache_request_headers() avec comparaison insensible à la casseLe JWT est signé HS256, clé JWT_SECRET. Le payload doit contenir company_uid et user_uid. Le cookie taskori_token est aussi accepté pour le navigateur web.
En cas d'échec, les requêtes JSON reçoivent un 401. Les requêtes navigateur sont redirigées vers TASKORI_AUTH_URL/login?redirect_uri=....
Tous les endpoints /api/internal/* vérifient :
$token = $_SERVER['HTTP_X_TASKORI_SHARE_TOKEN'] ?? '';
if (!hash_equals(TASKORI_SHARE_TOKEN, $token)) { ... 401 ... }
hash_equals protège contre les timing attacks.
L'accès via /s/{token} (64 hex chars) ne nécessite pas de JWT. Si un PIN est configuré, une session Redis est créée après vérification bcrypt, avec rate-limiting à 5 tentatives / 15 minutes par IP. La session est un cookie share_session (httponly, secure, samesite=Strict, TTL 2h).
Tous les endpoints sont sous /api/v1/, routés par api/v1/index.php.
| Méthode | Endpoint | Description |
|---|---|---|
| GET | /api/v1/me |
Profil de l'utilisateur connecté (email, nom, rôle, plan, timezone, company) |
| GET | /api/v1/storage |
Utilisation stockage du bucket company (used, quota en bytes) |
| GET | /api/v1/clients |
Liste des clients de la company (uid, name) — lecture depuis ProjectDB |
| GET | /api/v1/projects |
Liste des projets ; ?client_uid=xxx pour filtrer ; inclut client_uid et client_name si pas de filtre |
| GET | /api/v1/files |
Liste des fichiers : ?client_uid=&project_uid=&folder_uid= ; si folder_uid absent → fichiers sans dossier |
| DELETE | /api/v1/files/{uid} |
Soft-delete (deleted_at=NOW()) + décrémentation quota |
| GET | /api/v1/files/{uid}/download |
Téléchargement d'un fichier (déchiffrement à la volée, path traversal check) |
| POST | /api/v1/files/upload |
Upload multipart : file, client_uid, project_uid, folder_uid ; max 50 MB |
| GET | /api/v1/folders |
Liste des dossiers : ?client_uid=&project_uid=&parent_uid= ; racine si parent_uid absent |
| POST | /api/v1/folders |
Crée un dossier : {name, client_uid, project_uid, parent_uid} ; nom max 255 chars |
| DELETE | /api/v1/folders/{uid} |
Soft-delete du dossier |
| GET | /api/v1/links |
Liste les liens du bucket (tous états) ; ?project_uid=xxx pour filtrer ; ajoute url et has_pin |
| POST | /api/v1/links |
Crée un lien : {scope, file_uid, project_uid, folder_uid, pin, expiry} |
| DELETE | /api/v1/links/{uid} |
Révoque un lien (revoked_at=NOW()) |
{
"scope": "file|project|folder",
"file_uid": "...", // requis si scope=file
"project_uid": "...", // requis si scope=project
"folder_uid": "...", // requis si scope=folder
"pin": "1234", // optionnel, haché bcrypt
"expiry": "7d|30d|never"
}
Retourne { uid, token, url, expires_at }. L'URL est APP_URL + /s/ + token.
Ces endpoints sont appelés par les autres services Taskori (principalement taskori-manage et taskori-project) via X-Taskori-Share-Token.
| Méthode | Endpoint | Description |
|---|---|---|
| POST | /api/internal/upload.php |
Upload fichier encodé base64 : {company_uid, client_uid, project_uid, folder_uid, original_name, mime_type, plan, uploaded_by_uid, content} |
| GET | /api/internal/download.php |
Téléchargement par ?file_uid=&company_uid= |
| POST | /api/internal/delete.php |
Suppression physique + soft-delete DB : {file_uid, company_uid} |
| GET | /api/internal/bucket.php |
Info bucket : ?company_uid=&plan= → {storage_used, storage_quota, plan} |
| GET | /api/internal/list_project_files.php |
Fichiers d'un projet : ?company_uid=&project_uid= |
| POST | /api/internal/folder_get_or_create.php |
Retourne ou crée un dossier par nom : {company_uid, client_uid, project_uid, plan, name} — idempotent |
| POST | /api/internal/create_link.php |
Crée un lien de partage fichier (scope=file) : {file_uid, expiry} |
Différence upload interne vs v1 : L'upload interne accepte le contenu en base64 dans le body JSON (pratique pour les appels service-à-service), tandis que l'upload v1 utilise multipart/form-data.
Suppression interne vs v1 :
delete.phpinterne supprime physiquement le fichier du NFS en plus du soft-delete DB. Le DELETE v1 fait uniquement un soft-delete.
/s/{token})Routé par .htaccess vers s.php. Trois routes :
| URL | Action |
|---|---|
/s/{token} |
Vue principale (liste dossiers + fichiers) |
/s/{token}/upload |
Upload par client externe (POST) |
/s/{token}/download/{file_uid} |
Téléchargement par client externe |
L'interface supporte le drag-and-drop, la navigation dans les sous-dossiers (breadcrumb), la création de dossiers. Elle est responsive (Bootstrap 5, dark theme).
Fonctionnalités selon le scope du lien :
| Scope | Navigation dossiers | Upload | Téléchargement |
|---|---|---|---|
project |
Oui, tous les dossiers du projet | Oui | Oui |
folder |
Oui, sous-dossiers uniquement | Non | Oui |
file |
Non | Non | Fichier unique uniquement |
buckets| Colonne | Type | Description |
|---|---|---|
| id | INT UNSIGNED | PK auto-increment |
| company_uid | VARCHAR(36) | UNIQUE, identifiant company |
| plan | ENUM('solo','pro','business') | Plan actuel |
| encryption_key | TEXT | Clé bucket chiffrée par MASTER_KEY (base64) |
| storage_used | BIGINT | Octets consommés |
| storage_quota | BIGINT | Quota en octets selon le plan |
| created_at | DATETIME | Création |
files| Colonne | Type | Description |
|---|---|---|
| id | INT UNSIGNED | PK |
| uid | VARCHAR(36) | UNIQUE, identifiant public (hex 36 chars) |
| bucket_id | INT UNSIGNED | FK → buckets |
| client_uid | VARCHAR(36) | Optionnel |
| project_uid | VARCHAR(36) | Optionnel |
| folder_uid | VARCHAR(36) | NULL = racine |
| original_name | VARCHAR(255) | Nom d'origine du fichier |
| file_path | VARCHAR(500) | Chemin relatif depuis UPLOAD_PATH |
| file_size | BIGINT | Taille en bytes (non chiffré) |
| mime_type | VARCHAR(100) | MIME type détecté par finfo |
| uploaded_by_uid | VARCHAR(36) | UID user ou NULL (upload client externe) |
| uploaded_by_type | ENUM('user','client') | Source de l'upload |
| created_at | DATETIME | |
| deleted_at | DATETIME | NULL = actif (soft-delete) |
folders| Colonne | Type | Description |
|---|---|---|
| id | INT UNSIGNED | PK |
| uid | VARCHAR(36) | UNIQUE |
| bucket_id | INT UNSIGNED | FK → buckets |
| client_uid | VARCHAR(36) | Optionnel (dossier rattaché à un client sans projet) |
| project_uid | VARCHAR(36) | Optionnel |
| parent_uid | VARCHAR(36) | NULL = dossier racine |
| name | VARCHAR(255) | Nom |
| created_at | DATETIME | |
| deleted_at | DATETIME | Soft-delete |
share_links| Colonne | Type | Description |
|---|---|---|
| id | INT UNSIGNED | PK |
| uid | VARCHAR(36) | UNIQUE |
| bucket_id | INT UNSIGNED | FK → buckets |
| scope | ENUM('file','project','folder') | Portée du lien |
| file_id | INT UNSIGNED | NULL sauf scope=file |
| project_uid | VARCHAR(36) | NULL sauf scope=project/folder |
| folder_uid | VARCHAR(36) | NULL sauf scope=folder |
| token | VARCHAR(64) | UNIQUE, 32 bytes hex aléatoires |
| pin_hash | VARCHAR(255) | NULL si pas de PIN, sinon bcrypt |
| expires_at | DATETIME | NULL = pas d'expiration |
| created_by_uid | VARCHAR(36) | UID créateur (ou 'system' pour les liens internes) |
| created_at | DATETIME | |
| revoked_at | DATETIME | NULL = actif |
client_sessionsSessions temporaires pour les visiteurs de liens de partage (2h TTL) :
| Colonne | Description |
|---|---|
| share_token | Token du lien de partage |
| session_token | Token de session cookie |
| ip | IP du visiteur |
| expires_at | Expiration |
| Variable | Obligatoire | Description |
|---|---|---|
DB_HOST |
Oui | Hôte MariaDB |
DB_NAME |
Oui | Base taskori_share |
DB_USER |
Oui | Utilisateur DB share |
DB_PASS |
Oui | Mot de passe DB share |
JWT_SECRET |
Oui | Secret HS256 partagé avec taskori-auth |
MASTER_KEY |
Oui | 64 caractères hex (32 bytes) — chiffre toutes les clés bucket. Ne jamais changer après mise en production |
TASKORI_SHARE_TOKEN |
Oui | Token secret pour l'API interne (header X-Taskori-Share-Token) |
APP_URL |
Oui | URL publique (défaut : https://share.taskori.app) |
TASKORI_AUTH_URL |
Oui | URL du service auth (défaut : https://taskori.app) |
PROJECT_DB_NAME |
Oui | Nom de la DB manage/project |
PROJECT_DB_USER |
Oui | Utilisateur en lecture seule sur la DB project |
PROJECT_DB_PASS |
Oui | Mot de passe DB project |
UPLOAD_PATH |
Oui | Chemin de stockage fichiers (défaut : /opt/taskori-share/uploads) |
REDIS_HOST |
Non | Hôte Redis (défaut : redis) |
REDIS_PORT |
Non | Port Redis (défaut : 6379) |
SENTRY_DSN |
Non | DSN Sentry pour le monitoring d'erreurs |
MASTER_KEY est critique. Si elle est perdue ou changée sans re-chiffrement des clés bucket en DB, tous les fichiers deviennent irrécupérables. Elle est stockée dans les variables CI GitLab et injectée dans
.envau déploiement.
.gitlab-ci.ymltest → build → deploy
| Job | Déclencheur | Action |
|---|---|---|
lint |
Tout push | PHP syntax check sur tous les .php hors vendor |
build-dev |
Push sur dev |
Build image taguée dev, push registry LORVA |
deploy-dev |
Push sur dev |
docker compose -f docker-compose.dev.yml up -d |
build |
Push sur main |
Build image taguée main ET latest |
deploy |
Push sur main |
SCP .env + docker-compose.yml sur $DEPLOY_HOST, pull + restart |
Le job deploy :
.env en filtrant les variables CI commençant par DB_, AUTH_, JWT_, MASTER_, INTERNAL_, TASKORI_, etc..env et docker-compose.yml sur $DEPLOY_HOST:/docker/taskori-share/docker compose up -d via SSHNe jamais redémarrer le conteneur manuellement. Passer par un push Git sur
mainet surveiller le pipeline.
services:
web: # PHP 8.1 + Apache — image registry LORVA
redis: # Redis 7 Alpine — sessions PIN
Le conteneur web est accessible via Traefik sur share.taskori.app avec :
172.32.0.183 (réseau web) et 172.34.0.183 (réseau db)wud.watch=false — pas de mise à jour automatique par WUDentrypoint.sh :
config/config.php depuis config/conf.php.tpl via envsubstUPLOAD_PATH si absent, ajuste les permissions www-dataupload_max_filesize = 55M (marge sur la limite applicative de 50 MB)post_max_size = 57Mdisplay_errors = Offpdo_mysql, gmp, zip, gd, curl, redisNe doit jamais changer en production. Un changement invalide toutes les clés bucket stockées en DB et rend tous les fichiers irrécupérables. Si une rotation est nécessaire, il faut re-chiffrer toutes les lignes buckets.encryption_key en batch.
Apache mod_php supprime souvent l'en-tête Authorization. Le .htaccess contient une RewriteRule qui le passe via E=HTTP_AUTHORIZATION. L'auth_middleware.php a un triple fallback pour gérer les cas où ça ne marche pas.
deleted_at).delete.php fait à la fois le soft-delete ET supprime le fichier .enc du NFS.Le quota est mis à jour de façon synchrone à chaque upload/suppression. En cas d'erreur entre l'écriture disque et la mise à jour DB, storage_used peut être désynchronisé. La commande GREATEST(0, storage_used - ?) dans removeUsage évite les valeurs négatives.
DB (PDO singleton) pointe sur taskori_share. ProjectDB (autre PDO singleton) pointe sur la base de manage/project. Les appels cross-DB (ex : vérifier qu'un project_uid appartient à la company) font une requête sur ProjectDB.
Les tentatives de PIN sont rate-limitées via Redis (pin_attempts:{token}:{ip}, TTL 900s). Si Redis est indisponible, la vérification PIN échouera. Le service Redis est dans le même réseau Docker interne (taskori-share-network).
Les migrations sont appliquées manuellement (4 fichiers SQL dans /database/). Il n'y a pas de runner de migration automatique au démarrage du conteneur.
Les fichiers .enc sont stockés sur un NFS partagé (/mnt/NFS-data/taskori/share). Linux, macOS et Windows voient les mêmes fichiers. Ne pas créer de repo git dans ce répertoire.