REPLIT TASK — Expand Creator access + Persist Creator Assets (JSON DB)
What to ship

Access: Creator Dashboard allowed for roles: creator, owner, manager, admin.

DB layer: Add a lightweight JSON DB for creator assets at uploads/assets-db.json.

Endpoints: Fully implement:

POST /api/creator/assets/upload (already works) → now persists records.

GET /api/creator/assets/list?kind=photo|mockup|icon → returns my assets from DB.

DELETE /api/creator/assets/:id → soft-deletes (and tries to remove files if storage supports it).

Use our existing object storage adapter. Keep previews watermarked like admin uploads.

1) Allow creator/owner/manager/admin to access Creator Dashboard

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 DEFAULT_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 allowed = roles?.length ? roles : DEFAULT_ALLOWED;
  const userRoles: string[] = currentUser?.roles || [];
  const has = userRoles.some(r => allowed.includes(r));
  return has ? children : <Navigate to="/unauthorized" replace />;
}


Router snippet (Creator page)

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


Header link

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) JSON Asset Store (no new infra)

NEW: server/services/assetStore.js

const fs = require('fs');
const fsp = fs.promises;
const path = require('path');

const DB_DIR = path.join(process.cwd(), 'uploads');
const DB_PATH = path.join(DB_DIR, 'assets-db.json');

// naive mutex to serialize writes
let writing = Promise.resolve();

async function ensureDb() {
  await fsp.mkdir(DB_DIR, { recursive: true });
  try {
    await fsp.access(DB_PATH);
  } catch {
    const seed = { version: 1, assets: {}, byOwner: {} };
    await fsp.writeFile(DB_PATH, JSON.stringify(seed, null, 2), 'utf8');
  }
}

async function readDb() {
  await ensureDb();
  const raw = await fsp.readFile(DB_PATH, 'utf8');
  return JSON.parse(raw || '{}');
}

async function writeDb(next) {
  writing = writing.then(async () => {
    await ensureDb();
    const tmp = DB_PATH + '.tmp';
    await fsp.writeFile(tmp, JSON.stringify(next, null, 2), 'utf8');
    await fsp.rename(tmp, DB_PATH);
  }).catch(()=>{ /* swallow to keep chain alive */ });
  return writing;
}

async function upsertMany(records) {
  const db = await readDb();
  for (const r of records) {
    db.assets[r.id] = r;
    if (!db.byOwner[r.ownerId]) db.byOwner[r.ownerId] = [];
    if (!db.byOwner[r.ownerId].includes(r.id)) db.byOwner[r.ownerId].unshift(r.id);
  }
  await writeDb(db);
  return records;
}

async function getById(id) {
  const db = await readDb();
  return db.assets[id] || null;
}

async function listByOwner({ ownerId, kind }) {
  const db = await readDb();
  const ids = db.byOwner[ownerId] || [];
  const items = ids.map(id => db.assets[id]).filter(Boolean);
  return items
    .filter(a => !a.deleted && !a.hidden)
    .filter(a => (kind ? a.kind === kind : true))
    .sort((a,b) => (b.createdAt||0) - (a.createdAt||0));
}

async function softDelete({ id, ownerId }) {
  const db = await readDb();
  const rec = db.assets[id];
  if (!rec) return { ok:false, reason:'not_found' };
  if (String(rec.ownerId) !== String(ownerId)) return { ok:false, reason:'forbidden' };
  rec.deleted = true;
  rec.deletedAt = Date.now();
  await writeDb(db);
  return { ok:true, asset: rec };
}

module.exports = {
  upsertMany,
  getById,
  listByOwner,
  softDelete,
};

3) Wire DB into Creator Assets routes

EDIT: server/routes/creatorAssetRoutes.js
(Use this full file if easier; it replaces the previous draft. It persists uploads, lists, and deletes.)

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

const _storageMod = require('../objectStorage');
const storage = _storageMod?.default || _storageMod; // TS/JS compat

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

const KINDS = new Set(['photo','mockup','icon']);

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

async function makeWatermarkedPreview(buffer, _contentType, size = 1280) {
  const base = sharp(buffer).rotate().resize({ width: size, withoutEnlargement: true }).png();
  const meta = await base.metadata();
  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.12"/>
          <stop offset="100%" stop-color="#000000" stop-opacity="0.12"/>
        </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>
  `);
  const tilesX = Math.ceil((meta.width || size) / 400) + 2;
  const tilesY = Math.ceil((meta.height || size) / 200) + 2;
  const overlays = [];
  for (let ix = 0; ix < tilesX; ix++) {
    for (let iy = 0; iy < tilesY; iy++) {
      overlays.push({ input: tile, left: ix*400 - 200, top: iy*200 - 100 });
    }
  }
  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(overlays).png().toBuffer();

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

// All creator-area routes: allow creator/owner/manager/admin
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 baseName = file.originalname.replace(/\.[^.]+$/, '');
      const niceName = baseName.replace(/[_-]+/g, ' ').trim();

      // original key
      const origKey = (() => {
        if (kind === 'icon') {
          const folder = (file.mimetype === 'image/svg+xml' || ext === 'svg') ? 'svg' : 'png';
          return `${pathPrefix(kind)}/${folder}/${ownerId}/${id}/${file.originalname}`;
        }
        return `${pathPrefix(kind)}/${ownerId}/${id}/${file.originalname}`;
      })();

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

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

      saved.push({
        id, ownerId, kind,
        name: niceName,
        tags: tagArr,
        category: String(category || '').trim(),
        url, previewUrl,
        storageKeys: { original: origKey, preview: prevKey },
        createdAt: Date.now()
      });
    }

    await store.upsertMany(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 = req.query.kind ? String(req.query.kind) : undefined;
    if (kind && !KINDS.has(kind)) return res.status(400).json({ error: 'Invalid kind' });
    const assets = await store.listByOwner({ ownerId, kind });
    res.json({ assets });
  } catch (e) {
    console.error('creator list failed', 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);
    const rec = await store.getById(id);
    if (!rec) return res.status(404).json({ error: 'Not found' });
    if (String(rec.ownerId) !== String(ownerId) && !(req.user.roles||[]).includes('admin')) {
      return res.status(403).json({ error: 'Forbidden' });
    }

    const resp = await store.softDelete({ id, ownerId: rec.ownerId });

    // Try to physically delete if adapter supports it; otherwise, soft-delete is enough
    const canDelete = typeof storage.deleteObject === 'function';
    if (canDelete && rec.storageKeys) {
      const { original, preview } = rec.storageKeys;
      try { if (original) await storage.deleteObject(original); } catch {}
      try { if (preview)  await storage.deleteObject(preview);  } catch {}
    }

    return res.json({ ok: true, id, softDeleted: resp.ok, physicalDeleted: !!canDelete });
  } catch (e) {
    console.error('creator delete failed', e);
    res.status(500).json({ error: 'Delete failed' });
  }
});

module.exports = router;


server.js (ensure route is mounted)

const creatorAssetRoutes = require('./server/routes/creatorAssetRoutes');
// ...
app.use('/api/creator/assets', creatorAssetRoutes);

4) Quick tests
# List (empty)
curl -s -H "Authorization: Bearer <JWT>" \
  http://localhost:5000/api/creator/assets/list | jq .

# Upload one photo + one mockup
curl -s -X POST http://localhost:5000/api/creator/assets/upload \
  -H "Authorization: Bearer <JWT>" \
  -F kind=photo \
  -F tags="nature, forest" \
  -F category="backgrounds" \
  -F "files=@/path/to/photo1.jpg" | jq .
curl -s -X POST http://localhost:5000/api/creator/assets/upload \
  -H "Authorization: Bearer <JWT)" \
  -F kind=mockup \
  -F tags="apparel, tshirt" \
  -F category="fashion" \
  -F "files=@/path/to/mockup1.png" | jq .

# List again (should show assets)
curl -s -H "Authorization: Bearer <JWT>" \
  "http://localhost:5000/api/creator/assets/list?kind=photo" | jq .

# Delete one
curl -s -X DELETE -H "Authorization: Bearer <JWT>" \
  http://localhost:5000/api/creator/assets/<ASSET_ID> | jq .


Acceptance

Creator/Owner/Manager/Admin can access /creator.

Upload returns { ok:true, assets:[...] } with url & previewUrl.

List returns only the signed-in owner’s assets (optional kind filter).

Delete soft-deletes (and removes files if the storage adapter supports deleteObject).

Previews are watermarked the same as admin uploader.