Skip to content

Conversation

@kevalyq
Copy link
Contributor

@kevalyq kevalyq commented Nov 16, 2025

Summary

Refactors SecretAttachment API to use Laravel best practices: API Resources and Form Requests.

Changes

API Resource

  • Created SecretAttachmentResource for consistent JSON transformation
  • ISO 8601 date formatting via toIso8601String()
  • Decrypts filename_plain via accessor
  • Used in both store() and index() endpoints
  • PHPStan Level Max compliant with @mixin and explicit type casting

Form Request

  • Created StoreSecretAttachmentRequest for upload validation
  • Centralizes validation rules (was inline in controller)
  • Fixes byte-to-KB conversion for file size (config returns bytes, Laravel expects KB)
  • Custom error messages for better UX
  • PHPStan type hints for config values

Controller Refactoring

  • store(): Uses Form Request + API Resource
  • index(): Uses API Resource collection with proper JsonResponse wrapping
  • download(): Uses property accessor (filename_plain not getter method)
  • Removed inline validation and manual JSON building

Architecture Decision: NO Named Routes

  • NOT implemented: Named routes (originally planned in Issue Refactor SecretAttachment API to use Laravel Resources and Form Requests #180)
  • Rationale: Project uses OpenAPI contracts (contract-first architecture)
  • Consistency: NO named routes exist in 20+ existing routes
  • Pattern: All URL generation uses url() helper, not route()
  • Contracts: Frontend consumes exact paths from /contracts/docs/openapi.yaml

Test Coverage

  • ✅ 4 Resource transformation tests (single, collection, ISO8601, decryption)
  • ✅ 7 Form Request validation tests (required, type, size, MIME, messages)
  • ✅ All 342 tests passing (1085 assertions)
  • ✅ PHPStan Level Max passing (fixed 12 type errors)
  • ✅ Laravel Pint passing (PSR-12 compliant)

Helper Function Consolidation

  • Moved createTestSecret() and createTestAttachment() to tests/Pest.php
  • Ensures parallel test execution works correctly (no function redeclaration)
  • Follows same pattern as existing getTestKekPath() helper

Copilot Review Comments Addressed

From PR #177:

  • ✅ Thread PRRT_kwDOQJgcLc5iPRK2: Use API Resources (store method)
  • ✅ Thread PRRT_kwDOQJgcLc5iPRKm: Use API Resources (index method)
  • ✅ Thread PRRT_kwDOQJgcLc5iPRLg: Move validation to Form Request
  • ✅ Thread PRRT_kwDOQJgcLc5iPRLc: Use property accessor pattern
  • ❌ Thread PRRT_kwDOQJgcLc5iPRKv: Add named routes (NOT implemented - architecture decision)
  • ❌ Thread PRRT_kwDOQJgcLc5iPRKf: Use route() helper (NOT implemented - architecture decision)

Quality Gates

All checks passing:

  • ✅ PHPStan Level Max: No errors (12 type issues fixed)
  • ✅ Laravel Pint: PSR-12 compliant
  • ✅ REUSE 3.3: Compliant
  • ✅ Domain Policy: Passed
  • ✅ Full Test Suite: 342 tests, 1085 assertions

Related Issues

Closes #180
Related: #175 (File Attachments API - Phase 2)
Related: #177 (File Attachments API implementation)

- Add create_secret_attachments_table migration (2025_11_16_110234)
- UUID primary key, foreign keys to secrets/users (CASCADE)
- Encrypted filename_enc field, file metadata (size, mime, storage_path, checksum)
- Indexes on secret_id and (secret_id, created_at)
- Add comprehensive migration tests (7 tests, 18 assertions)
- Tests verify table structure, column types, FK constraints, indexes, cascade behavior

Part of #175 (Phase 2: File Attachments API)
- Create SecretAttachment model with EncryptedWithDek cast for filename
- UUID primary key, transient filename_plain property
- Relationships: belongsTo Secret, belongsTo User (uploader)
- Hidden fields: filename_enc, storage_path
- Download URL accessor for API routes
- Add basic model tests (fillable fields, hidden fields)

Part of #175 (Phase 2: File Attachments API)
- Implements store() with tenant DEK encryption (JSON blob: ciphertext + nonce)
- Implements retrieve() with decryption from storage
- Implements delete() for file and DB record removal
- Storage: attachments/{tenant_id}/{secret_id}/{uuid}.enc
- Fixed: tenant_id required in secret_attachments table for EncryptedWithDek cast
- Fixed: explicit property assignment pattern for encrypted fields
- Tests: 3 passing (store, encrypt, retrieve/decrypt)
- Migration: added tenant_id column with foreign key constraint

Related: #175
- Add hasMany relationship to SecretAttachment
- Add attachment_count accessor for convenience
- Tests: 2 new tests (relationship + accessor)
- All SecretTest.php tests passing (13 total)

Related: #175
- Implement viewAny, view, create, delete authorization
- Only secret owners can manage attachments (sharing support TODO)
- Register policy in AppServiceProvider
- Tests: 8 passing (owner/non-owner scenarios)
- Note: Factory removed (EncryptedWithDek incompatible with mass assignment)

Related: #175
- POST /v1/secrets/{secret}/attachments (upload with validation)
- GET /v1/secrets/{secret}/attachments (list)
- GET /v1/attachments/{attachment}/download (with headers)
- DELETE /v1/attachments/{attachment}
- Config: attachments.php (max_file_size, allowed_mime_types)
- Tests: 13 passing (upload, list, download, delete, authorization)
- Standards: /v1/ prefix, no named routes, correct copyright format

Related: #175
- Use 'mimetypes:' instead of 'mimes:' for true MIME type validation
- Remove mimeToExtension helper (no longer needed)
- Fix test file SPDX header to use 'SecPal Contributors' format
- Security: Prevents upload of malicious files with fake extensions
- Use 'SecPal Contributors' format
- Matches project standard across all files
- Use 'SecPal Contributors' format
- Matches project standard
- Added entry for Issue #175 (Phase 2)
- Documents all 4 endpoints and key features
- Notes encryption, authorization, and test coverage

Related: #175
- Add User import to Controller
- Add type annotations for config values
- Add null checks for file_get_contents, json_encode, tenantKey
- Add null check for getMimeType()
- Add runtime exceptions for validation failures
- Fix BelongsTo return types using $this
- Simplify delete() return type (always returns true)
- Cast division to int for max file size validation

All changes maintain runtime behavior while satisfying static analysis.
- Add explicit check and type guard for validated['file']
- Add type annotation for tenant_id assignment
- Add string type validation for decoded ciphertext/nonce before base64_decode

All 13 Controller tests still passing.
- PHPStan now understands $validated is array<string, mixed>
- Resolves final offsetAccess.nonOffsetAccessible error
- Secret::create() doesn't follow explicit property pattern
- SecretAttachment::create() doesn't set tenant_id before encrypted fields
- 5 Model unit tests failing due to encryption pattern violations
- Controller tests (13/13) all passing
- Will create separate issue to fix Model tests properly

Related: #175
Fixes #179

- Add createTestAttachment() helper function
- Replace all SecretAttachment::create() with createTestAttachment()
- Ensures tenant_id is set BEFORE encrypted fields (filename_plain)
- Required for EncryptedWithDek cast to work correctly
…, header escaping)

- Use config('attachments.storage_disk') instead of hardcoded 'local' (3 locations)
- Add checksum verification in retrieve() method for file integrity
- Escape Content-Disposition filename to prevent header injection
- Fix Storage::assertMissing() test syntax (remove disk parameter)
- Add PHPStan type annotations for config() return values

Addresses Copilot review comments:
- Storage disk hardcoded (lines 69, 102, 139)
- Missing checksum verification (line 126) - SECURITY
- Unescaped Content-Disposition header (line 116) - SECURITY
- Incorrect test syntax (line 255)

Tests: 13 Controller + 3 Service tests passing
Quality: PHPStan Level Max + Pint compliant

Related: #175, #179
- Enhanced migration tenant_id documentation for EncryptedWithDek cast
- Removed unused $test parameters from test helper functions
- Fixed SPDX header format consistency (SecPal Contributors)
- Added security warning for empty MIME types array in config
- Added file existence check in delete() method (graceful handling)
- Optimized attachment_count accessor to prevent N+1 queries
- Removed SecretAttachmentResource (deferred to Issue #180)

Addresses Copilot review comments:
- Migration documentation (PRRT_kwDOQJgcLc5iPRKb)
- Unused parameters (PRRT_kwDOQJgcLc5iPRLC, PRRT_kwDOQJgcLc5iPRLl)
- SPDX format (PRRT_kwDOQJgcLc5iPRLh)
- Config security (PRRT_kwDOQJgcLc5iPRLX)
- File existence check (PRRT_kwDOQJgcLc5iPRKp)
- N+1 optimization (PRRT_kwDOQJgcLc5iPRK9)

Related: #175, #177, #180
- Create SecretAttachmentResource for consistent JSON transformation
- Create StoreSecretAttachmentRequest with config-based validation
- Refactor SecretAttachmentController to use Resource and Form Request
- Remove inline validation and manual JSON array building
- Fix byte-to-KB conversion for file size validation
- Use property accessor pattern (filename_plain vs getter method)
- Add PHPStan type hints (@var, @mixin) for Level Max compliance

Tests added:
- 4 Resource transformation tests (single, collection, ISO8601, decryption)
- 7 Form Request validation tests (required, type, size, MIME, messages)
- Consolidate helper functions in tests/Pest.php for parallel test isolation

Quality gates:
- PHPStan Level Max: PASSING (fixed 12 type errors)
- Laravel Pint: PASSING (PSR-12 compliant)
- Full test suite: 342 tests passing (1085 assertions)

Architecture note:
- Maintained url() helper usage (NOT route() named routes)
- Project uses OpenAPI contracts, not Laravel route names
- Consistent with existing 20+ routes in routes/api.php

Addresses Copilot review comments from PR #177:
- Thread PRRT_kwDOQJgcLc5iPRK2: Use API Resources (store method)
- Thread PRRT_kwDOQJgcLc5iPRKm: Use API Resources (index method)
- Thread PRRT_kwDOQJgcLc5iPRLg: Move validation to Form Request
- Thread PRRT_kwDOQJgcLc5iPRLc: Use property accessor pattern

Related: #175, #177, #180
Copilot AI review requested due to automatic review settings November 16, 2025 15:51
@github-actions
Copy link

💡 Tip: Consider Using Draft PRs

Benefits of opening PRs as drafts initially:

  • 💰 Saves CI runtime and Copilot review credits
  • 🎯 Automatically sets linked issues to "🚧 In Progress" status
  • 🚀 Mark "Ready for review" when done to trigger full CI pipeline

How to convert:

  1. Click "Still in progress? Convert to draft" in the sidebar, OR
  2. Use gh pr ready when ready for review

This is just a friendly reminder - feel free to continue as is! 😊

@codecov
Copy link

codecov bot commented Nov 16, 2025

Codecov Report

❌ Patch coverage is 92.77108% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
app/Services/AttachmentStorageService.php 86.36% 9 Missing ⚠️
app/Models/Secret.php 71.42% 2 Missing ⚠️
.../Controllers/Api/V1/SecretAttachmentController.php 96.87% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copilot finished reviewing on behalf of kevalyq November 16, 2025 15:55
@kevalyq kevalyq added the large-pr-approved Legitimate large PR (e.g., boilerplate templates, auto-generated code) label Nov 16, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR successfully refactors the SecretAttachment API to use Laravel best practices by introducing API Resources and Form Requests. The implementation includes comprehensive test coverage, proper encryption handling, and follows the project's architecture decisions (contract-first, no named routes).

Key Changes

  • Created SecretAttachmentResource for consistent JSON transformation with ISO 8601 date formatting
  • Created StoreSecretAttachmentRequest centralizing upload validation rules
  • Refactored controller to use Form Request + API Resource pattern
  • Added helper functions to tests/Pest.php for parallel test execution

Reviewed Changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/Pest.php Added createTestSecret() and createTestAttachment() helper functions to prevent parallel test execution conflicts
tests/Feature/Services/AttachmentStorageServiceTest.php Tests for encryption, decryption, and storage service operations
tests/Feature/SecretTest.php Tests for Secret model attachments relationship and count accessor
tests/Feature/Resources/SecretAttachmentResourceTest.php Tests for API Resource transformation, collection handling, ISO8601 formatting, and decryption
tests/Feature/Requests/StoreSecretAttachmentRequestTest.php Tests for Form Request validation rules including file requirements, size limits, and MIME types
tests/Feature/Policies/SecretAttachmentPolicyTest.php Tests for owner-based authorization policies (viewAny, view, create, delete)
tests/Feature/Models/SecretAttachmentTest.php Tests for model structure, UUID keys, encryption, relationships, and accessors
tests/Feature/Migrations/CreateSecretAttachmentsTableTest.php Tests for migration schema validation including columns, types, indexes, and foreign keys
tests/Feature/Controllers/SecretAttachmentControllerTest.php Integration tests for all controller endpoints (upload, list, download, delete) with authorization
routes/api.php Added four SecretAttachment endpoints under v1 API group
database/migrations/2025_11_16_110234_create_secret_attachments_table.php Migration creating secret_attachments table with UUID, foreign keys, and encrypted fields
config/attachments.php Configuration file defining max file size, allowed MIME types, and storage disk
app/Services/AttachmentStorageService.php Service handling file encryption, storage, retrieval, and deletion with checksum verification
app/Providers/AppServiceProvider.php Registered SecretAttachmentPolicy with Gate
app/Policies/SecretAttachmentPolicy.php Authorization policy restricting access to secret owners only
app/Models/SecretAttachment.php Model with EncryptedWithDek cast for filename, UUID keys, relationships, and download_url accessor
app/Models/Secret.php Added attachments relationship and attachment_count accessor to Secret model
app/Http/Resources/SecretAttachmentResource.php API Resource transforming attachment metadata with decrypted filename and ISO8601 dates
app/Http/Requests/StoreSecretAttachmentRequest.php Form Request with validation rules for file upload (size, MIME types from config)
app/Http/Controllers/Api/V1/SecretAttachmentController.php Controller using Form Request, API Resource, Policy, and Service for all attachment operations
CHANGELOG.md Documents File Attachments API Phase 2 features and test coverage

Add missing blank line after @Property annotation to comply with PSR-12.
- Use mimetypes validation instead of mimes for MIME type validation
- Add explicit null check for filename in download method
- Cast file_size to string for Content-Length header

Resolves 3 Copilot review comments in PR #181.
Copy link
Contributor Author

@kevalyq kevalyq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot Comments Addressed:

Comment 1 (mimetypes): Changed from mimes to mimetypes validation rule to properly accept full MIME types directly.

Comment 2 (filename null check): Added explicit null check that aborts with 500 error if filename is missing.

Comment 3 (Content-Length type): Cast file_size to string with (string) for Content-Length header.

All fixes committed in 60cc525.

@kevalyq kevalyq merged commit dae0e76 into main Nov 16, 2025
21 checks passed
@kevalyq kevalyq deleted the feat/issue-180-attachment-api-refactoring branch November 16, 2025 16:14
kevalyq added a commit that referenced this pull request Nov 16, 2025
Resolved conflicts by accepting refactored versions from PR #181:
- SecretAttachmentController now uses API Resources + Form Requests
- SecretAttachmentTest now uses consolidated Pest helpers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

large-pr-approved Legitimate large PR (e.g., boilerplate templates, auto-generated code)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor SecretAttachment API to use Laravel Resources and Form Requests

2 participants