prompt for the Replit team that (1) lets creator, owner, and manager see the Creator Dashboard, and (2) adds /api/creator/assets upload/list endpoints scoped to the signed-in creator (with auto-watermarked previews like our admin uploader).

Replit prompt — Expand creator access + creator asset uploads
0) What to ship

Allow roles creator, owner, manager (and admin) to access the Creator Dashboard.

Add Creator Assets API under /api/creator/assets:

POST /upload — multi-file upload; kinds: photo | mockup | icon

GET /list?kind= — list my assets by kind (owner-scoped)

(optional) DELETE /:id — remove my asset

Store original file; generate watermarked preview (same visual treatment as admin uploader); persist metadata {id, ownerId, kind, name, tags, category, url, previewUrl, createdAt}.

Use existing object storage adapter (the one used for stock photos/mockups/icons). Keep paths consistent:

Photos: stock/photos/{ownerId}/{id}/{filename}

Mockups: stock/mockups/{ownerId}/{id}/{filename}

Icons: icons/{format}/{ownerId}/{id}/{filename} + icons/preview/{ownerId}/{id}/{filename}.png

Dependencies to add (server): multer, sharp, and reuse our existing objectStorage util.

1) Role guard — allow creator/owner/manager/admin

server/middleware/requireRole.js

module.exports = function requireRole(...allowed) {
  return (req, res, next) => {
    if (!req.user) return res.status(401).json({ error: 'Auth required' });
    const roles = req.user.roles || [];
    const ok = roles.includes('admin') || roles.some(r => allowed.includes(r));
    if (!ok) return res.status(403).json({ error: 'Forbidden' });
    next();
  };
};


src/components/auth/RequireRole.tsx

import { Navigate } from "react-router-dom";
import { useAuth } from "@/contexts/AuthContext";

const ALLOWED = ['creator','owner','manager','admin'];

export default function RequireRole({ roles, children }: { roles?: string[]; children: JSX.Element }) {
  const { currentUser, loading } = useAuth();
  if (loading) return null;
  const userRoles: string[] = currentUser?.roles || [];
  const allowed = roles && roles.length ? roles : ALLOWED;
  const has = userRoles.some(r => allowed.includes(r));
  return has ? children : <Navigate to="/unauthorized" replace />;
}


router (where we mount the dashboard)

<Route
  path="/creator"
  element={
    <RequireRole roles={['creator','owner','manager','admin']}>
      <CreatorDashboard />
    </RequireRole>
  }
/>


Header (show link for any allowed role)

const roles: string[] = currentUser?.roles || [];
const canSeeCreator = roles.some(r => ['creator','owner','manager','admin'].includes(r));
{canSeeCreator && <a href="/creator" className="text-sm px-3 py-2 rounded hover:bg-gray-100">Creator</a>}

2) Creator Assets API

server/routes/creatorAssetRoutes.js

const express = require('express');
const multer = require('multer');
const sharp = require('sharp');
const { randomUUID } = require('crypto');
const requireRole = require('../middleware/requireRole');

// IMPORTANT: reuse our existing object storage adapter.
// If it's TypeScript default export, require accordingly.
const storage = require('../objectStorage'); // ensure it exposes putObject(key, buffer, contentType) and publicUrl(key)

const router = express.Router();
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 }});

// Kinds we support
const KINDS = new Set(['photo','mockup','icon']);

// derive key prefixes per kind
function pathPrefix(kind) {
  if (kind === 'photo')  return 'stock/photos';
  if (kind === 'mockup') return 'stock/mockups';
  if (kind === 'icon')   return 'icons';
  return 'misc';
}

// very light watermark: diagonal tiled text similar to admin uploader
async function makeWatermarkedPreview(buffer, contentType, size = 1280) {
  // Always land on PNG for preview
  const base = await sharp(buffer).rotate().resize({ width: size, withoutEnlargement: true }).png();

  // Simple tiled text watermark using SVG overlay (cheap & fast)
  const tile = Buffer.from(`
    <svg xmlns="http://www.w3.org/2000/svg" width="400" height="200">
      <defs>
        <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
          <stop offset="0%" stop-color="#ffffff" stop-opacity="0.08"/>
          <stop offset="100%" stop-color="#000000" stop-opacity="0.08"/>
        </linearGradient>
      </defs>
      <rect width="100%" height="100%" fill="transparent"/>
      <g transform="rotate(-20 200 100)">
        <text x="0" y="80" font-family="Inter, Arial" font-size="36" fill="url(#g)" letter-spacing="2">IBrandBiz</text>
        <text x="120" y="160" font-family="Inter, Arial" font-size="36" fill="url(#g)" letter-spacing="2">IBrandBiz</text>
      </g>
    </svg>
  `);

  // Composite tile repeatedly by using sharp's tile-like trick via metadata+overlay with gravity,
  // but simplest is: create a big watermark canvas then composite once.
  const meta = await base.metadata();
  const wmCanvas = await sharp({
    create: { width: meta.width || size, height: meta.height || size, channels: 4, background: { r:0,g:0,b:0,alpha:0 } }
  })
  .composite(Array.from({length: Math.ceil((meta.width||size)/400)+2}, (_, ix) =>
    Array.from({length: Math.ceil((meta.height||size)/200)+2}, (_, iy) => ({
      input: tile, left: ix*400 - 200, top: iy*200 - 100
    }))
  ).flat())
  .png()
  .toBuffer();

  const out = await base
    .composite([{ input: wmCanvas, gravity: 'center' }])
    .png({ quality: 85 })
    .toBuffer();

  return { buffer: out, contentType: 'image/png' };
}

// owner guard
router.use(requireRole('creator','owner','manager','admin'));

// POST /api/creator/assets/upload
// form-data: kind=photo|mockup|icon, tags (csv), category, files[]
router.post('/upload', upload.array('files', 25), async (req, res) => {
  try {
    const ownerId = String(req.user.id);
    const { kind, tags = '', category = '' } = req.body || {};
    if (!KINDS.has(kind)) return res.status(400).json({ error: 'Invalid kind' });
    if (!req.files?.length) return res.status(400).json({ error: 'No files' });

    const tagArr = String(tags).split(',').map(t => t.trim()).filter(Boolean);
    const saved = [];

    for (const file of req.files) {
      const id = randomUUID().replace(/-/g,'');
      const ext = (file.originalname.split('.').pop() || '').toLowerCase();
      const origKey = (() => {
        if (kind === 'icon') {
          // support svg/png uploads for icons
          const folder = (file.mimetype === 'image/svg+xml' || ext === 'svg') ? 'svg' : 'png';
          return `${pathPrefix(kind)}/${folder}/${ownerId}/${id}/${file.originalname}`;
        }
        // photos/mockups
        return `${pathPrefix(kind)}/${ownerId}/${id}/${file.originalname}`;
      })();

      // store original
      await storage.putObject(origKey, file.buffer, file.mimetype);
      const url = await storage.publicUrl(origKey);

      // preview (always PNG)
      const { buffer: prevBuf, contentType: prevType } = await makeWatermarkedPreview(file.buffer, file.mimetype, 1280);
      const prevKey = (kind === 'icon')
        ? `icons/preview/${ownerId}/${id}/${file.originalname.replace(/\.[^.]+$/, '')}.png`
        : `${pathPrefix(kind)}/preview/${ownerId}/${id}/${file.originalname.replace(/\.[^.]+$/, '')}.png`;
      await storage.putObject(prevKey, prevBuf, prevType);
      const previewUrl = await storage.publicUrl(prevKey);

      saved.push({
        id, ownerId, kind,
        name: file.originalname.replace(/\.[^.]+$/, '').replace(/[_-]+/g, ' ').trim(),
        tags: tagArr, category: String(category || '').trim(),
        url, previewUrl, createdAt: Date.now()
      });
    }

    // Persist metadata (reuse whatever store we have; if a function exists, swap here)
    // Example: await assetStore.insertMany(saved)
    res.json({ ok: true, count: saved.length, assets: saved });
  } catch (e) {
    console.error('creator upload failed', e);
    res.status(500).json({ error: 'Upload failed' });
  }
});

// GET /api/creator/assets/list?kind=photo|mockup|icon
router.get('/list', async (req, res) => {
  try {
    const ownerId = String(req.user.id);
    const kind = String(req.query.kind || '');
    if (kind && !KINDS.has(kind)) return res.status(400).json({ error: 'Invalid kind' });
    // Replace with real query against your DB (ownerId + optional kind)
    // Example: const items = await assetStore.find({ ownerId, ...(kind?{kind}:{}) }).sort({createdAt:-1})
    res.json({ assets: [], note: 'TODO: wire to DB; endpoint is ready' });
  } catch (e) {
    res.status(500).json({ error: 'List failed' });
  }
});

// DELETE /api/creator/assets/:id
router.delete('/:id', async (req, res) => {
  try {
    const ownerId = String(req.user.id);
    const id = String(req.params.id);
    // Example: check ownership then delete originals + preview keys (you can store keys in DB)
    // await assetStore.deleteOne({ id, ownerId })
    res.json({ ok: true, id });
  } catch (e) {
    res.status(500).json({ error: 'Delete failed' });
  }
});

module.exports = router;


Wire the routes in server.js

const creatorAssetRoutes = require('./server/routes/creatorAssetRoutes');

// keep /api/stripe webhook before body parser, as we already do
app.use('/api/creator/assets', creatorAssetRoutes);

3) Notes for Replit

Reuse the existing objectStorage module that powers Stock Photos/Mockups/Icons (paths above). If its export shape differs, adapt the putObject/publicUrl calls accordingly.

The preview generator uses sharp and a lightweight SVG tile watermark to match our admin look. If we already have a dedicated watermark util, feel free to swap it in the makeWatermarkedPreview() function.

Frontend upload can reuse the Admin StockUploader UI: when a user with role creator | owner | manager | admin opens it, POST to /api/creator/assets/upload instead of the admin endpoints.

4) Quick test (cURL)
# Upload two mockups
curl -X POST http://localhost:5000/api/creator/assets/upload \
  -H "Authorization: Bearer <JWT>" \
  -F kind=mockup \
  -F tags="interior, living room" \
  -F category="home" \
  -F "files=@/path/one.png" \
  -F "files=@/path/two.jpg"


This gives you { ok: true, count: 2, assets:[...] } with url and previewUrl.