Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1514601
PM-2648: Support wrapped PM role lookups for application emails
jmgasper Jun 18, 2026
cb30bcb
Merge pull request #24 from topcoder-platform/PM-2648
jmgasper Jun 18, 2026
9010b5f
initialize prisma migrations
vas3a Jun 24, 2026
4de30be
PM-5306 - data model for project showcase posts
vas3a Jun 24, 2026
4afb37c
PM-5307 - project showcase post API
vas3a Jun 25, 2026
9fd009a
PM-5307 - move metadata, rename status enum
vas3a Jun 25, 2026
0d3354c
lint & pr feedback
vas3a Jun 26, 2026
a07b8b0
PM-5307 - add tests for showcase posts
vas3a Jun 26, 2026
f4e9c1e
deploy to dev
vas3a Jun 26, 2026
4ec7332
run prisma migrate deploy on CI/CD
vas3a Jun 26, 2026
0d2049a
Run migrate via startup.sh
vas3a Jun 26, 2026
56a3ce5
PM-5307 - allow copilot to manage showcase posts
vas3a Jun 29, 2026
c11ba30
PM-5307 - project showcase post pagination
vas3a Jun 29, 2026
718cda0
PM-5307 - throw error when provided industry or category are invalid
vas3a Jun 29, 2026
048dd6a
validate challenge id
vas3a Jun 29, 2026
85bf8d2
Merge pull request #25 from topcoder-platform/PM-5307_project-showcas…
vas3a Jun 29, 2026
dbd0d19
PM-5485 - media assets for project showcase
vas3a Jun 29, 2026
9ed7b7b
pr feedback
vas3a Jun 30, 2026
c2cd5e9
Merge pull request #26 from topcoder-platform/PM-5485_project-showcas…
vas3a Jun 30, 2026
6ed48fe
PM-5307 - fix swagger for project showcase posts
vas3a Jun 30, 2026
cfe9aeb
Merge pull request #27 from topcoder-platform/PM-5307_fix-swagger
vas3a Jun 30, 2026
c217dd2
PM-5487 - provide signed urls for the project showcase media
vas3a Jul 1, 2026
ac16b71
cleanup
vas3a Jul 1, 2026
280d0b1
docs
vas3a Jul 1, 2026
a7d5dba
PM-5487 - env example
vas3a Jul 1, 2026
5264258
undo pnpm-lock changes related to tc-core
vas3a Jul 1, 2026
2a73ec1
Merge pull request #28 from topcoder-platform/PM-5487_showcase-media-…
vas3a Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ workflows:
branches:
only:
- dev
tags:
only: /^dev-.*/

build-prod:
jobs:
Expand Down
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,8 @@ CORS_ALLOWED_ORIGIN=""

# Logging
NODE_ENV=development

CLOUDFRONT_PROJECT_SHOWCASE_MEDIA_PRIVATE_KEY=""
CLOUDFRONT_PROJECT_SHOWCASE_MEDIA_PUBLIC_KEY=""
CLOUDFRONT_PROJECT_SHOWCASE_MEDIA_KEY_PAIR_ID=""
CLOUDFRONT_PROJECT_SHOWCASE_MEDIA_URL_EXPIRATION=""
3 changes: 3 additions & 0 deletions appStartUp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ set -eo pipefail

export DATABASE_URL=$(echo -e ${DATABASE_URL})

echo "Database - running migrations."
npx prisma migrate deploy

# Start the app
exec node dist/src/main
273 changes: 273 additions & 0 deletions docs/projects-showcase-media.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
# CloudFront Signed URLs + Private S3 Setup

## Purpose

This document describes how to configure a private S3 bucket that is
only accessible through CloudFront using CloudFront Signed URLs.

The application authenticates users with JWTs. CloudFront **does not**
validate JWTs directly. Instead:

1. The backend validates the JWT.
2. The backend generates a short-lived CloudFront signed URL.
3. CloudFront validates the signature.
4. CloudFront retrieves the object from the private S3 bucket using an
Origin Access Control (OAC).

------------------------------------------------------------------------

# Architecture

``` text
Browser
|
| JWT
v
Projects API
|
| Verify JWT
| Generate CloudFront Signed URL
v
Browser
|
| GET https://cdn.example.com/path/file.ext?...Signature...
v
CloudFront
|
| Validate signature
| (automatic)
v
Origin Access Control (OAC)
|
| SigV4 request
v
Private S3 Bucket
```

------------------------------------------------------------------------

# S3 Configuration

## Bucket

- Enable **Block all public access**.
- Do not use public ACLs.
- Do not add bucket policies granting `Principal: "*"` read access.

## Bucket Policy

Replace the placeholders before applying.

``` json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::YOUR_BUCKET/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::YOUR_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID"
}
}
}
]
}
```

------------------------------------------------------------------------

# CloudFront Configuration

## 1. Origin Access Control

Create:

CloudFront
└── Origin access
└── Origin access controls

Configuration:

- Origin type: S3
- Signing behavior: Sign requests
- Signing protocol: SigV4

Attach the OAC to the S3 origin.

------------------------------------------------------------------------

## 2. RSA Key Pair

Generate locally:

``` bash
openssl genrsa -out private_key.pem 2048

openssl rsa \
-pubout \
-in private_key.pem \
-out public_key.pem
```

Never commit `private_key.pem`.

------------------------------------------------------------------------

## 3. Public Key

CloudFront
└── Security
└── Public Keys

Create a Public Key and paste the contents of `public_key.pem`.

Record the generated **Key Pair ID**.

------------------------------------------------------------------------

## 4. Key Group

CloudFront
└── Security
└── Key Groups

Create a Key Group containing the Public Key.

------------------------------------------------------------------------

## 5. Distribution

Edit the protected behavior.

Configure:

- Viewer protocol policy: Redirect HTTP to HTTPS
- Trusted Key Groups: Select the Key Group created above

Once configured, CloudFront automatically:

- rejects unsigned requests
- rejects expired requests
- rejects requests signed with an unknown key

No Lambda or custom verification logic is required.

------------------------------------------------------------------------

# Parameter Store

Store the following values in AWS Systems Manager Parameter Store.

/config/projects-api-v6/appvar/CLOUDFRONT_PROJECT_SHOWCASE_MEDIA_PRIVATE_KEY
/config/projects-api-v6/appvar/CLOUDFRONT_PROJECT_SHOWCASE_MEDIA_PUBLIC_KEY
/config/projects-api-v6/appvar/CLOUDFRONT_PROJECT_SHOWCASE_MEDIA_KEY_PAIR_ID

Recommended types:

| Parameter | Type | Notes |
|---|---|---|
| CLOUDFRONT_PROJECT_SHOWCASE_MEDIA_PRIVATE_KEY | SecureString | PEM contents of private key |
| CLOUDFRONT_PROJECT_SHOWCASE_MEDIA_PUBLIC_KEY | String | PEM contents of public key |
| CLOUDFRONT_PROJECT_SHOWCASE_MEDIA_KEY_PAIR_ID | String | CloudFront Key Pair ID |

The application should read these values at startup and never hardcode
or commit them.

------------------------------------------------------------------------

# Backend

Install:

``` bash
npm install @aws-sdk/cloudfront-signer
```

Example:

``` ts
import { getSignedUrl } from "@aws-sdk/cloudfront-signer";

const signedUrl = getSignedUrl({
url,
keyPairId: process.env.CLOUDFRONT_PROJECT_SHOWCASE_MEDIA_KEY_PAIR_ID!,
privateKey: process.env.CLOUDFRONT_PROJECT_SHOWCASE_MEDIA_PRIVATE_KEY!,
dateLessThan: new Date(Date.now() + 5 * 60 * 1000).toISOString()
});
```

`getSignedUrl()` performs local cryptographic signing only. It does not
make any AWS API calls.

------------------------------------------------------------------------

# Request Flow

``` text
Client
├── JWT
Projects API
├── Verify JWT
├── getSignedUrl()
Signed URL
CloudFront
├── Validate signature
├── Validate expiration
S3 (via OAC)
```

------------------------------------------------------------------------

# Validation

Expected results:

Request Expected
--------------------------------------- -------------------
Direct S3 URL 403 Access Denied
CloudFront URL without signature 403 Forbidden
CloudFront URL with invalid signature 403 Forbidden
CloudFront URL with expired signature 403 Forbidden
CloudFront URL with valid signature 200 OK

------------------------------------------------------------------------

# Key Rotation

1. Generate a new RSA key pair.
2. Create a new CloudFront Public Key.
3. Add the new Public Key to the existing Key Group.
4. Update Parameter Store with the new private key and key pair ID.
5. Deploy the backend.
6. Wait until all previously issued signed URLs have expired.
7. Remove the old Public Key from the Key Group.
8. Delete the old key material if no longer required.

This sequence avoids downtime during key rotation.

------------------------------------------------------------------------

# Security Checklist

- Block Public Access enabled.
- No public bucket policy.
- OAC attached to the S3 origin.
- Bucket policy only permits the CloudFront distribution.
- Private key stored as SecureString in Parameter Store.
- Private key never committed to source control.
- Signed URLs expire after a short period (recommended: 5--15
minutes).
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:load": "jest --config ./test/jest-e2e.json --runInBand --testRegex \"test/load/performance.test.ts$\"",
"test:deployment": "jest --config ./test/jest-e2e.json --runInBand test/deployment-validation.e2e-spec.ts",
"deploy:dev": "BRANCH=$(git rev-parse --abbrev-ref HEAD) && TAG=\"dev-${BRANCH}\" && git tag -d \"$TAG\" 2>/dev/null; git push origin \":refs/tags/$TAG\" 2>/dev/null; git tag \"$TAG\" && git push origin \"$TAG\"",
"postinstall": "npx prisma generate"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.926.0",
"@aws-sdk/cloudfront-signer": "^3.1077.0",
"@aws-sdk/s3-request-presigner": "^3.926.0",
"@nestjs/axios": "^4.0.0",
"@nestjs/common": "^11.0.1",
Expand Down
29 changes: 29 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading