Skip to content

Add profile media kit and appearance controls#149

Merged
BASIC-BIT merged 8 commits into
mainfrom
profile-media-kit-assets
Jun 17, 2026
Merged

Add profile media kit and appearance controls#149
BASIC-BIT merged 8 commits into
mainfrom
profile-media-kit-assets

Conversation

@BASIC-BIT

@BASIC-BIT BASIC-BIT commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds file-backed profile media-kit assets, upload/import routes, public asset/logo APIs, and S3-backed storage helpers.
  • Adds bounded avatar appearance controls with account editing, public profile rendering, and fixture/demo coverage.
  • Updates profile/search cards, docs, backend tests, and snapshot baselines for media-kit presentation.

Notes

  • Storage routes return 501 until the profile asset bucket/region environment variables are configured.
  • HTTPS logo imports are copied into managed storage instead of used as canonical hotlinks.
  • AI review findings around upload bounds, import SSRF hardening, revocable asset caching, and search-card media rendering have been addressed.

Verification

  • Latest CI, visual diff checks, and local recycle checks are green.

@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vr-dex-web Ready Ready Preview, Comment Jun 17, 2026 7:28am

Request Review

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

Storybook Component Screenshot Preview

Outcome: success
Run: https://github.com/BASIC-BIT/VRDex/actions/runs/27672935607
Artifact: storybook-component-preview

Screenshots: primitive component stories captured on desktop and mobile.

This lane is separate from full-route Playwright screenshots and focuses on design-system component regressions.

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

Storybook Image Diff

Outcome: success
Run: https://github.com/BASIC-BIT/VRDex/actions/runs/27672935607
Artifact: storybook-image-diff

Changed Storybook baselines: none in this PR.

This check compares design-system component screenshots against committed baselines. Inline images show only added or modified Storybook baseline PNGs.

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

Playwright Hosted Data-Flow

Outcome: success
Target: https://staging.vrdex.net
Hosted extended profile flow: enabled
Hosted auth helpers: enabled
Hosted adapter helpers: enabled
Run: https://github.com/BASIC-BIT/VRDex/actions/runs/27672935607
Artifact: playwright-hosted-data-flow

This optional check runs the mutation-backed profile flow against a configured hosted dev/staging target with isolated E2E test data.

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

Playwright Data-Flow Preview

Outcome: success
Run: https://github.com/BASIC-BIT/VRDex/actions/runs/27672935607
Artifact: playwright-data-flow

Captured flow:

  • test-gated profile submission form
  • gated helper rejection without the Playwright token
  • Convex profile creation
  • submission success state
  • public profile page readback
  • discovery search readback

Artifacts include screenshots, traces, and recorded video for the flow run.

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

Playwright Public Screenshot Preview

Outcome: success
Run: https://github.com/BASIC-BIT/VRDex/actions/runs/27672935607
Artifact: playwright-public-preview

Screenshots: all public route checks passed on desktop and mobile.

Full screenshot set is available in the artifact. Pixel diff baselines are handled by the separate Playwright Image Diff check.

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

Playwright Image Diff

Outcome: success
Run: https://github.com/BASIC-BIT/VRDex/actions/runs/27672935607
Artifact: playwright-image-diff

Changed screenshot baselines: 32

desktop-chromium / account-signed-out

desktop-chromium account-signed-out

desktop-chromium / appearance-demo

desktop-chromium appearance-demo

desktop-chromium / community-profile

desktop-chromium community-profile

desktop-chromium / deployment

desktop-chromium deployment

desktop-chromium / event-edit-signed-out

desktop-chromium event-edit-signed-out

desktop-chromium / event-new-signed-out

desktop-chromium event-new-signed-out

desktop-chromium / event-profile

desktop-chromium event-profile

desktop-chromium / event-watch-surface

desktop-chromium event-watch-surface

desktop-chromium / home

desktop-chromium home

desktop-chromium / person-profile

desktop-chromium person-profile

desktop-chromium / privacy-suppression

desktop-chromium privacy-suppression

desktop-chromium / search

desktop-chromium search

Showing 12 of 32; see the artifact for the rest.

This check compares public route screenshots against committed baselines. Inline images show only added or modified baseline PNGs.

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

@BASIC-BIT BASIC-BIT marked this pull request as ready for review June 16, 2026 20:51

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd592dabeb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/web/src/app/api/v0/profile-assets/upload-intents/[intentId]/route.ts Outdated
@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a complete profile media-kit system: S3-backed asset upload/import with streaming SSRF protection, per-profile avatar appearance controls, public asset/logo/zip API routes, and matching frontend rendering on profile and account pages.

  • Upload pipeline: file uploads and HTTPS URL imports are handled through an upload-intent lifecycle in Convex; the Next.js route resolves DNS upfront, pins the TCP connection to the resolved IP to prevent DNS rebinding, validates MIME type and byte size while streaming, then writes to S3.
  • Avatar appearance: borderEnabled, borderColor, borderWidthPx, borderSoftnessPx, and radiusPercent are normalized and clamped server-side; rendered as CSS inline styles with a live deferred preview on the account page.
  • Public APIs and rendering: asset file proxy enforces CSP: sandbox; script-src 'none' for SVG safety; logos zip is built with a hand-rolled, dependency-free ZIP writer; profile and search cards now overlay media-kit images over legacy avatar URLs as a backward-compatible migration path.

Confidence Score: 5/5

Safe to merge; the one finding is a minor hardening gap in the SSRF IP blocklist for a long-deprecated IPv6 range with no practical presence in modern infrastructure.

The upload pipeline correctly prevents DNS rebinding, streams with a hard byte cap, validates MIME types, and gates all mutations behind Convex auth and upload tokens. The S3 client cache is now keyed by region, SVG assets are served under a CSP sandbox, and the avatar appearance controls are clamped server-side. The only gap found — fec0::/10 not being blocked in isPrivateIpv6 — applies only to deprecated site-local IPv6 addresses that have not been allocated since 2004.

The SSRF IP-validation logic in apps/web/src/app/api/v0/profile-assets/upload-intents/[intentId]/route.ts (isPrivateIpv6) deserves a second look to close the fec0::/10 gap before the URL import feature reaches production.

Important Files Changed

Filename Overview
apps/web/src/app/api/v0/profile-assets/upload-intents/[intentId]/route.ts New upload route with SSRF defenses (DNS pinning, private-IP blocklist, streaming size cap). One minor gap: deprecated site-local IPv6 range fec0::/10 not blocked.
apps/web/src/lib/server/profile-asset-storage.ts S3 helper with per-region client caching, public immutable cache headers on PUT, error handling for NoSuchKey. Clean and safe.
apps/web/src/app/api/v0/profiles/[slug]/logos.zip/route.ts Parallel Convex+S3 fetches via Promise.all, sanitized filenames, correct ArrayBuffer slice. 5-minute public cache is a deliberate trade-off for this rarely-changing bundle.
apps/web/src/app/api/v0/profiles/[slug]/assets/[assetId]/file/route.ts Asset file proxy with CSP sandbox for SVG safety, nosniff, sanitized filename, correct content-length fallback. No auth needed (enforced by visibility=public check in Convex).
convex/profileAssets.ts Upload intent lifecycle, avatar appearance mutation with ownership check, and public media kit query. Auth and ownership gates are correctly placed.
convex/_profileAssets.ts Core helpers: SSRF URL validation (HTTPS-only, no credentials), bounded avatar appearance normalization, storage key generation with 96-bit token entropy.
convex/schema.ts Adds four new tables: profileAssetUploadIntents, profileAssets, profileAssetPlacements, profileAssetDisplayPreferences. Indexes are well-targeted and consistent with query patterns.
convex/profiles.ts submitCommunityProfile extended to accept and attach asset uploads; only creates new unclaimed profiles so immediate public visibility is appropriate. getPublicBySlug now overlays media kit images over legacy URLs.
apps/web/src/lib/avatar-appearance.ts Pure CSS-property generator for avatar frame; borderWidth as a number is correct (React appends px for length CSS properties).
apps/web/src/app/account/appearance/appearance-panel.tsx Client-side avatar appearance editor with live preview, deferred rendering, error boundary, and demo mode. Save is correctly gated on non-demo + non-saving state.
apps/web/src/lib/server/zip.ts Hand-rolled ZIP writer using STORED compression (method 0). CRC32 table, local/central headers, and EOCD record are structurally correct. 16-bit entry count is fine given the per-profile logo cap.
apps/web/src/app/api/e2e/fixture-assets/[assetId]/route.ts Playwright fixture SVG generator; gated behind VRDEX_ENABLE_PLAYWRIGHT_FIXTURES and blocks production unless VRDEX_ALLOW_PRODUCTION_E2E_HELPERS=true. SVG text is properly escaped.
apps/web/src/app/_components/profile-public-page.tsx Adds media kit section with parallel logo cards and zip download link; avatar now uses avatarFrameStyle with graceful fallback to defaultAvatarAppearance.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant NextAPI as Next.js API Routes
    participant Convex
    participant S3

    Note over Client,S3: Upload Flow
    Client->>Convex: createUploadIntent(mimeType, fileName/sourceUrl)
    Convex-->>Client: intentId + uploadToken + storageKey

    alt File Upload
        Client->>NextAPI: "POST /upload-intents/{intentId} multipart/form-data"
        NextAPI->>Convex: validateUploadIntentForStorage
        NextAPI->>S3: putProfileAssetObject
        NextAPI->>Convex: markUploadIntentUploaded
    else URL Import
        Client->>NextAPI: "POST /upload-intents/{intentId} sourceUrl intent"
        NextAPI->>NextAPI: DNS resolve + IP blocklist check
        NextAPI->>ExternalServer: HTTPS GET pinned to resolved IP
        ExternalServer-->>NextAPI: streamed bytes up to 12 MB
        NextAPI->>S3: putProfileAssetObject
        NextAPI->>Convex: markUploadIntentUploaded
    end

    Client->>Convex: submitCommunityProfile with assets
    Convex->>Convex: consumeProfileAssetUploads active+public

    Note over Client,S3: Retrieval Flow
    Client->>NextAPI: "GET /profiles/{slug}/assets/{assetId}/file"
    NextAPI->>Convex: getPublicAssetForStorage
    Convex-->>NextAPI: storageKey for public active asset
    NextAPI->>S3: GetObjectCommand
    NextAPI-->>Client: bytes with CSP sandbox header

    Client->>NextAPI: "GET /profiles/{slug}/logos.zip"
    NextAPI->>Convex: listPublicBySlug
    NextAPI->>Convex: getPublicAssetForStorage x N parallel
    NextAPI->>S3: getProfileAssetObject x N parallel
    NextAPI-->>Client: ZIP archive
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant NextAPI as Next.js API Routes
    participant Convex
    participant S3

    Note over Client,S3: Upload Flow
    Client->>Convex: createUploadIntent(mimeType, fileName/sourceUrl)
    Convex-->>Client: intentId + uploadToken + storageKey

    alt File Upload
        Client->>NextAPI: "POST /upload-intents/{intentId} multipart/form-data"
        NextAPI->>Convex: validateUploadIntentForStorage
        NextAPI->>S3: putProfileAssetObject
        NextAPI->>Convex: markUploadIntentUploaded
    else URL Import
        Client->>NextAPI: "POST /upload-intents/{intentId} sourceUrl intent"
        NextAPI->>NextAPI: DNS resolve + IP blocklist check
        NextAPI->>ExternalServer: HTTPS GET pinned to resolved IP
        ExternalServer-->>NextAPI: streamed bytes up to 12 MB
        NextAPI->>S3: putProfileAssetObject
        NextAPI->>Convex: markUploadIntentUploaded
    end

    Client->>Convex: submitCommunityProfile with assets
    Convex->>Convex: consumeProfileAssetUploads active+public

    Note over Client,S3: Retrieval Flow
    Client->>NextAPI: "GET /profiles/{slug}/assets/{assetId}/file"
    NextAPI->>Convex: getPublicAssetForStorage
    Convex-->>NextAPI: storageKey for public active asset
    NextAPI->>S3: GetObjectCommand
    NextAPI-->>Client: bytes with CSP sandbox header

    Client->>NextAPI: "GET /profiles/{slug}/logos.zip"
    NextAPI->>Convex: listPublicBySlug
    NextAPI->>Convex: getPublicAssetForStorage x N parallel
    NextAPI->>S3: getProfileAssetObject x N parallel
    NextAPI-->>Client: ZIP archive
Loading

Reviews (3): Last reviewed commit: "Address final profile asset review findi..." | Re-trigger Greptile

Comment thread apps/web/src/lib/server/profile-asset-storage.ts Outdated
Comment thread convex/_profileAssets.ts Outdated
@BASIC-BIT

Copy link
Copy Markdown
Owner Author

AI review recycle note: Greptile's outside-diff ZIP latency item was also addressed in bde1773 by fetching per-logo storage records/objects with Promise.all before building the archive.

@BASIC-BIT

Copy link
Copy Markdown
Owner Author

@codex review
@greptile review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bde1773b8e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/web/src/app/api/v0/profile-assets/upload-intents/[intentId]/route.ts Outdated
@BASIC-BIT

Copy link
Copy Markdown
Owner Author

@codex review
@greptile review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18cbf982b0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/web/src/app/api/v0/profiles/[slug]/assets/[assetId]/file/route.ts Outdated
Comment thread convex/_profileAssets.ts
Comment thread apps/web/src/app/_components/discovery-public-page.tsx Outdated
@BASIC-BIT

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f05592f025

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/web/src/app/api/v0/profile-assets/upload-intents/[intentId]/route.ts Outdated
Comment thread convex/profileAssets.ts
@BASIC-BIT

Copy link
Copy Markdown
Owner Author

AI review recycle note: Greptile's updated summary noted the deprecated IPv6 site-local fec0::/10 range. Addressed in 075e1a8 by adding that range to the SSRF private IPv6 blocklist.

@BASIC-BIT BASIC-BIT merged commit 877c47e into main Jun 17, 2026
22 checks passed
@BASIC-BIT BASIC-BIT deleted the profile-media-kit-assets branch June 17, 2026 07:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant