Marketing Planning - Media Upload¶
Overview¶
The Marketing Planning module supports direct file uploads for campaign media assets. Files are uploaded to S3 via the Media API and served through CloudFront. This replaces the previous workflow where users had to manually upload files to S3 and paste URLs.
Architecture¶
┌──────────┐ ┌────────────┐ ┌───────────────┐ ┌─────────────┐
│ Browser │─────>│ MediaApi │─────>│ MediaService │─────>│ S3 Bucket │
│ (File) │ XHR │ /marketing │ │ uploadMktMedia│ │ tq-media │
└──────────┘ │ /upload │ └───────┬───────┘ └──────┬──────┘
└────────────┘ │ │
│ │
CDN URL ◄─┘ CloudFront │
returned serves │
to client publicly ▼
Key Design Decisions¶
| Decision | Choice | Rationale |
|---|---|---|
| Upload path | Server-side PutObject | All files stream through the API server for validation |
| Image processing | None — stored as-is | Marketing assets need original quality; CDN browser handles optimization |
| MIME detection | Tika magic bytes | Cannot trust client-supplied Content-Type or file extensions |
| Prefix structure | campaigns/{slug}-{id}/ |
Derived server-side from campaign DB record; client sends only campaignId |
| Credentials | AWS DefaultCredentialsProvider | Same config across EC2 (instance role), local dev (~/.aws/credentials), CI (env vars) |
Implementation¶
Backend Components¶
| Component | Location | Purpose |
|---|---|---|
MediaApi.java |
tqapi/.../api/ |
REST endpoint: POST /media/marketing/upload (multipart) |
MediaService.java |
tqapp/.../service/media/ |
MIME validation, size checks, S3 upload, filename generation |
MediaS3Config.java |
tqapp/.../service/media/ |
S3Client singleton, purpose/template registry |
MediaConfig JAXB classes |
tqapp/.../framework/media/ |
Configuration: purposes, templates, file type limits |
Configuration¶
Media configuration lives in config/nts-client.xml under <MediaConfig>. The marketing-campaigns purpose includes a <MarketingUploader> element with accepted file types and size limits:
<Purpose id="marketing-campaigns"
bucket="tq-media"
prefix="campaigns/"
cdnBaseUrl="https://cdn.tripqlub.com">
<MarketingUploader>
<FileType type="image" mimeTypes="image/jpeg,image/png,image/webp,image/gif,image/tiff" maxMb="20"/>
<FileType type="pdf" mimeTypes="application/pdf" maxMb="10"/>
<FileType type="audio" mimeTypes="audio/mpeg,audio/mp4,audio/wav,audio/ogg" maxMb="50"/>
<FileType type="video" mimeTypes="video/mp4,video/quicktime,video/x-msvideo,video/webm" maxMb="500"/>
</MarketingUploader>
</Purpose>
Frontend Integration¶
The media dialog in mktplan.html includes a file upload zone below the URL field. Users can either enter a URL manually or upload a file directly.
Upload flow:
1. User selects a file via the file input in the #media_dlg dialog
2. Clicks the "Upload" button
3. uploadMarketingFile() in mktplan.js sends a FormData via XHR to /media/marketing/upload
4. Progress bar shows upload progress
5. On success, the returned CDN URL is populated into the #media_url field
6. User clicks "Add" to save the media record (existing addMedia() flow)
Media list display:
- After saving, the media appears in the #media_list panel within the activity detail view
- Image and graphic media types show a thumbnail loaded from the CDN URL
- Clicking a thumbnail or icon opens the media file in a new browser tab
- Video and other types show a type-specific icon (also clickable if a URL exists)
- Each item has a delete button to remove it from the activity
Auth: Uses getAuthHeaders() from globals.js for the OIDC access token. Content-Type is omitted (multipart sets its own boundary).
API Roles¶
File Processing Details¶
| Step | Detail |
|---|---|
| 1. Campaign lookup | Load campaign by ID from DB; derive slug prefix |
| 2. MIME detection | org.apache.tika.Tika.detect(bytes) — magic-byte based |
| 3. Type validation | Match against <FileType> groups in config |
| 4. Size validation | Check against per-type maxMb limit |
| 5. Basename sanitization | Lowercase, [a-z0-9-] only, max 64 chars, no path traversal |
| 6. Extension derivation | Static MIME-to-extension table (client extension ignored) |
| 7. Filename generation | {basename}-{8-hex-suffix}.{ext} |
| 8. S3 upload | PutObject with validated Content-Type and 1-year Cache-Control |
| 9. Response | CDN URL, key, filename, size, contentType, fileType |
Security¶
| Threat | Mitigation |
|---|---|
| Malicious file disguised as image | Tika magic-byte detection; reject if MIME not in allowed set |
| Path traversal via basename | Sanitized to [a-z0-9-] only; .., /, \ stripped |
| Campaign prefix manipulation | Only campaignId from client; slug derived from DB record server-side |
| MIME type spoofing | Content-Type header and file extension ignored; Tika detects from bytes |
| Oversized uploads | Per-type size check before S3 upload; Jetty 500 MB limit as backstop |
| AWS credential leak | DefaultCredentialsProvider; no credentials in config files |
API Reference¶
See Media API Specification for full endpoint documentation.