Skip to content

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

media/marketing/upload=agent,admin

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.