2) routes/stockLibraryRoutes.js
// routes/stockLibraryRoutes.js
// Upload + serve stock photos with server-side watermarking for previews/unlicensed

const path = require("path");
const fs = require("fs/promises");
const express = require("express");
const multer = require("multer");
const { applyWatermark, toTenDigitCode } = require("../services/watermark");
const ObjectStorageService = require("../services/ObjectStorageService"); // your existing wrapper

const router = express.Router();

// --- Multer storage (temp to local before pushing to object storage) ---
const uploadDir = path.join(process.cwd(), "uploads", "photos");
const storage = multer.diskStorage({
  destination: async (_req, _file, cb) => {
    await fs.mkdir(uploadDir, { recursive: true });
    cb(null, uploadDir);
  },
  filename: (_req, file, cb) => {
    const stamp = Date.now();
    const safe = file.originalname.replace(/\s+/g, "_");
    cb(null, `${stamp}_${safe}`);
  },
});
const upload = multer({
  storage,
  limits: { fileSize: 25 * 1024 * 1024 }, // 25MB
});

// Helper: where we store in object storage
function objKeyOriginal(dbId, filename) {
  return `stock/original/${dbId}/${filename}`;
}
function objKeyPreview(dbId, filename) {
  return `stock/preview/${dbId}/${filename}`;
}

// NOTE: You likely already insert a DB row for each stock asset.
// Here we mock a DB insert and return a stable id.
async function insertStockDBRecord({ originalName, mimeType, uploaderId }) {
  // Replace with your real DB call; return { id, ... }
  const id = Math.floor(Date.now() / 1000).toString(36) + Math.floor(Math.random()*1e6).toString(36);
  return { id, originalName, mimeType, uploaderId };
}

// POST /api/stock/upload (admin)
router.post("/upload", upload.single("file"), async (req, res) => {
  try {
    if (!req.file) return res.status(400).json({ error: "No file uploaded" });

    // 1) Create DB record to get stable id (or use your real DB).
    const dbRow = await insertStockDBRecord({
      originalName: req.file.originalname,
      mimeType: req.file.mimetype,
      uploaderId: req.user?.id || "admin", // adapt to your auth
    });

    const tenDigit = toTenDigitCode(dbRow.id);

    // 2) Push ORIGINAL to object storage
    const originalBuf = await fs.readFile(req.file.path);
    const originalKey = objKeyOriginal(dbRow.id, req.file.filename);
    await ObjectStorageService.putObject(originalKey, originalBuf, req.file.mimetype, { privacy: "private" });

    // 3) Generate WATERMARKED PREVIEW (Adobe-style)
    const wmBuf = await applyWatermark(originalBuf, { stockSeed: dbRow.id, opacity: 0.15 });
    const previewKey = objKeyPreview(dbRow.id, req.file.filename);
    await ObjectStorageService.putObject(previewKey, wmBuf, req.file.mimetype, { privacy: "public" });

    // 4) Clean local temp
    await fs.unlink(req.file.path).catch(() => {});

    // 5) Return metadata your frontend expects
    const previewUrl = await ObjectStorageService.getPublicUrl(previewKey);
    res.json({
      id: dbRow.id,
      code: tenDigit,
      name: req.file.originalname,
      mimeType: req.file.mimetype,
      previewUrl,
      originalKey, // keep private
    });
  } catch (err) {
    console.error("Upload error:", err);
    res.status(500).json({ error: "Upload failed" });
  }
});

// GET /api/stock/:id/preview
// Serves the already-watermarked preview (public). Frontend can use this in the modal zoom.
router.get("/:id/preview", async (req, res) => {
  try {
    const { id } = req.params;
    // In a real app, look up filename; here we list by prefix and take first
    const list = await ObjectStorageService.listPrefix(`stock/preview/${id}/`);
    if (!list.length) return res.status(404).send("Not found");
    const key = list[0].key;
    const stream = await ObjectStorageService.getStream(key);
    res.setHeader("Content-Type", list[0].contentType || "image/jpeg");
    stream.pipe(res);
  } catch (err) {
    console.error(err);
    res.status(500).send("Preview error");
  }
});

// GET /api/stock/:id/download
// If user has NOT purchased, return watermarked preview.
// If purchased, return original (private).
router.get("/:id/download", async (req, res) => {
  try {
    const { id } = req.params;
    const userId = req.user?.id || null;

    // TODO: replace with your actual purchase/entitlement check
    const hasLicense = false; // await checkUserLicense(userId, id)

    if (!hasLicense) {
      const list = await ObjectStorageService.listPrefix(`stock/preview/${id}/`);
      if (!list.length) return res.status(404).send("Not found");
      const key = list[0].key;
      const stream = await ObjectStorageService.getStream(key);
      res.setHeader("Content-Disposition", `inline; filename="preview_${id}.jpg"`);
      res.setHeader("Content-Type", list[0].contentType || "image/jpeg");
      return stream.pipe(res);
    }

    // Licensed: serve original
    const listO = await ObjectStorageService.listPrefix(`stock/original/${id}/`);
    if (!listO.length) return res.status(404).send("Not found");
    const keyO = listO[0].key;
    const streamO = await ObjectStorageService.getStream(keyO);
    res.setHeader("Content-Disposition", `attachment; filename="IBrandBiz_${id}.jpg"`);
    res.setHeader("Content-Type", listO[0].contentType || "image/jpeg");
    streamO.pipe(res);
  } catch (err) {
    console.error(err);
    res.status(500).send("Download error");
  }
});

module.exports = router;

Replit prompt (paste this to your agent)

Implement Adobe-style server-side watermarking for the Stock Photo system. Use Sharp to composite an SVG overlay that includes:

One large diagonal “IBrandBiz” label across the center,

A grid of smaller diagonal “IBrandBiz” labels around it,

A vertical left-edge label “IBrandBiz | ###########” where the 10-digit code is derived from the DB record id (stable) via a SHA-256 hash → digits.
The watermark should auto-choose white or black based on image brightness (Sharp stats) and render at ~15% opacity.
Save originals privately to object storage and public previews as watermarked versions. For unlicensed downloads, return the watermarked preview; for licensed, return original.
Use the provided services/watermark.js and routes/stockLibraryRoutes.js files verbatim; wire the router under /api/stock in the Express app. Ensure Multer stays at 25MB. Keep existing ObjectStorageService. Install/update dependency: sharp.

Frontend usage (no big changes)

Thumbnail grid continues to use the preview URL returned by /upload or GET /api/stock/:id/preview.

Your fullscreen modal keeps showing that same preview URL; it will now display with the diagonal watermarks and the vertical stock code on the left.

“Download Full Size” should call GET /api/stock/:id/download.