PD-46 — Décomposition en tâches agents¶
Story : Implémenter download sécurisé avec pre-signed URLs Date : 2026-02-11 Branche : feature/PD-46-secure-download
Contexte existant¶
| Module existant | Statut | Impact |
|---|---|---|
modules/storage/s3.service.ts | TODOs, méthodes stub | Étendre avec getSignedUrl() |
modules/audit/audit.service.ts | Fonctionnel (logEvent()) | Utiliser directement |
modules/documents/ | Existe | Ajouter sous-module download/ |
Tâches par agent¶
| # | Agent | Module | Fichier(s) | Contract | Dépendances |
|---|---|---|---|---|---|
| 1 | agent-developer | download-dto | download.dto.ts | DownloadResponseDto | - |
| 2 | agent-developer | download-errors | download.errors.ts | DownloadErrorCode, DownloadError | - |
| 3 | agent-developer | share-repository | share.repository.ts (extension) | findActiveShare() | - |
| 4 | agent-sre | s3-presign-service | s3-presign.service.ts | S3PresignService.getSignedUrl() | @aws-sdk/s3-request-presigner |
| 5 | agent-developer | audit-download-service | audit-download.service.ts | AuditDownloadService | audit.service.ts |
| 6 | agent-developer | document-access-guard | document-access.guard.ts | DocumentAccessGuard | share-repository, download-errors |
| 7 | agent-developer | download-service | download.service.ts | DownloadService | s3-presign, audit-download |
| 8 | agent-developer | download-controller | download.controller.ts | DownloadController | download-service, guard |
| 9 | agent-qa-unit-integration | download-tests | __tests__/*.spec.ts | TC-NOM-, TC-ERR-, TC-INV-* | Tous modules |
Détail des tâches¶
Tâche 1 — download-dto (agent-developer)¶
Fichier : src/modules/documents/download/download.dto.ts
Contract :
export class DownloadResponseDto {
@ApiProperty({ description: 'Pre-signed URL pour téléchargement direct' })
downloadUrl: string;
@ApiProperty({ description: 'Date d\'expiration de l\'URL (UTC)' })
expiresAt: Date;
}
Invariants : - Critères : CA-46-01, CA-46-07
Tâche 2 — download-errors (agent-developer)¶
Fichier : src/modules/documents/download/download.errors.ts
Contract :
export enum DownloadErrorCode {
UNAUTHORIZED = 'ERR-46-001',
FORBIDDEN = 'ERR-46-002',
SHARE_REVOKED = 'ERR-46-003',
TENANT_MISMATCH = 'ERR-46-004',
INSUFFICIENT_ROLE = 'ERR-46-005',
NOT_FOUND = 'ERR-46-006',
S3_ERROR = 'ERR-46-007',
AUDIT_ERROR = 'ERR-46-008',
}
export class DownloadError extends Error {
constructor(
public readonly code: DownloadErrorCode,
message: string,
public readonly httpStatus: number,
) {
super(message);
}
}
Invariants : - Critères : CA-46-03
Tâche 3 — share-repository (agent-developer)¶
Fichier : src/modules/shares/share.repository.ts (extension ou création)
Contract :
async findActiveShare(
userId: string,
documentId: string,
): Promise<Share | null> {
// SELECT * FROM shares
// WHERE user_id = $1 AND document_id = $2
// AND is_active = true
// AND (expires_at IS NULL OR expires_at > NOW())
// AND revoked_at IS NULL
}
Invariants : INV-46-02 Critères : CA-46-02, CA-46-04
Note : Vérifier si modules/shares/ existe, sinon vérifier la structure des partages dans documents.
Tâche 4 — s3-presign-service (agent-sre)¶
Fichier : src/modules/documents/storage/s3-presign.service.ts
Contract :
@Injectable()
export class S3PresignService {
private readonly s3Client: S3Client;
private readonly bucket: string = 'probatiovault-documents';
constructor(private readonly configService: ConfigService) {
// Initialiser S3Client avec credentials OVH (depuis ConfigService)
// Endpoint: s3.gra.perf.cloud.ovh.net
}
async getSignedUrl(
key: string,
options: { expiresIn: number } = { expiresIn: 300 },
): Promise<string> {
// Utiliser @aws-sdk/s3-request-presigner
// getSignedUrl(s3Client, new GetObjectCommand({Bucket, Key}), {expiresIn})
// IMPORTANT: Ne lit PAS le contenu, génère seulement l'URL
}
}
Invariants : INV-46-01, INV-46-06 Critères : CA-46-07, CA-46-09
Dépendances npm : @aws-sdk/client-s3, @aws-sdk/s3-request-presigner
Tâche 5 — audit-download-service (agent-developer)¶
Fichier : src/modules/documents/audit/audit-download.service.ts
Contract :
@Injectable()
export class AuditDownloadService {
constructor(private readonly auditService: AuditService) {}
async logDownloadSuccess(
userId: string,
documentId: string,
tenantId?: string,
): Promise<void> {
// Event: DOWNLOAD_SUCCESS
// Utiliser auditService.logEvent()
}
async logDownloadDenied(
userId: string,
documentId: string,
reason: string,
tenantId?: string,
): Promise<void> {
// Event: DOWNLOAD_DENIED
// Utiliser auditService.logEvent()
}
}
Invariants : INV-46-04, INV-46-05 Critères : CA-46-05, CA-46-06
Tâche 6 — document-access-guard (agent-developer)¶
Fichier : src/modules/documents/download/guards/document-access.guard.ts
Contract :
@Injectable()
export class DocumentAccessGuard implements CanActivate {
constructor(
private readonly shareRepo: ShareRepository,
private readonly documentRepo: DocumentRepository,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// 1. Extraire userId du JWT (request.user)
// 2. Extraire documentId des params
// 3. Vérifier ownership (document.ownerId === userId)
// 4. Si non owner: vérifier partage actif via shareRepo.findActiveShare()
// 5. Pour B2B (OIDC): validateTenant + validateRole
// 6. Throw DownloadError si refusé
}
}
Invariants : INV-46-01, INV-46-02 Critères : CA-46-01, CA-46-02, CA-46-03, CA-46-04
Tâche 7 — download-service (agent-developer)¶
Fichier : src/modules/documents/download/download.service.ts
Contract :
@Injectable()
export class DownloadService {
constructor(
private readonly s3PresignService: S3PresignService,
private readonly auditService: AuditDownloadService,
private readonly documentRepo: DocumentRepository,
) {}
async generateDownloadUrl(
documentId: string,
user: UserContext,
): Promise<{ url: string; expiresAt: Date }> {
// 1. Récupérer document (s3_key)
// 2. Appeler s3PresignService.getSignedUrl(key, {expiresIn: 300})
// 3. Logger via auditService.logDownloadSuccess()
// 4. Retourner {url, expiresAt: now + 5min}
}
}
Invariants : INV-46-01, INV-46-04, INV-46-05 Critères : CA-46-05, CA-46-06, CA-46-07
Tâche 8 — download-controller (agent-developer)¶
Fichier : src/modules/documents/download/download.controller.ts
Contract :
@Controller('documents')
@UseGuards(JwtAuthGuard, DocumentAccessGuard)
export class DownloadController {
constructor(private readonly downloadService: DownloadService) {}
@Get(':id/download')
@HttpCode(HttpStatus.OK)
async getDownloadUrl(
@Param('id', ParseUUIDPipe) documentId: string,
@CurrentUser() user: UserContext,
): Promise<DownloadResponseDto> {
const result = await this.downloadService.generateDownloadUrl(documentId, user);
return {
downloadUrl: result.url,
expiresAt: result.expiresAt,
};
}
}
Invariants : INV-46-01, INV-46-06 Critères : CA-46-01, CA-46-02, CA-46-03
Tâche 9 — download-tests (agent-qa-unit-integration)¶
Fichiers : - src/modules/documents/download/__tests__/download.controller.spec.ts - src/modules/documents/download/__tests__/download.service.spec.ts - src/modules/documents/download/__tests__/document-access.guard.spec.ts
Tests à couvrir : - TC-NOM-01, TC-NOM-02, TC-NOM-03 (flux nominaux) - TC-ERR-01 à TC-ERR-06 (erreurs) - TC-INV-01, TC-INV-02, TC-INV-04, TC-INV-05 (invariants)
Invariants : Tous (INV-46-01 à 06) Critères : Tous (CA-46-01 à 09)
Ordre d'exécution¶
Phase 1 (parallèle): Tâches 1, 2
Phase 2: Tâche 3
Phase 3: Tâche 4
Phase 4: Tâche 5
Phase 5: Tâche 6
Phase 6: Tâche 7
Phase 7: Tâche 8
Phase 8: Tâche 9
Points d'attention¶
| Point | Impact | Action |
|---|---|---|
Module shares inexistant | Tâche 3 | Vérifier structure actuelle, adapter |
| S3Service existant stub | Tâche 4 | Créer nouveau service dédié presign |
| DocumentRepository existant | Tâches 6, 7 | Identifier et importer |
| JwtAuthGuard existant | Tâche 8 | Identifier et importer |
| UserContext existant | Tâches 7, 8 | Identifier type/decorator |
Généré par : Claude Opus 4.5 Date : 2026-02-11