import { Router } from "express"; import { pool } from "../db.js"; import { v4 as uuidv4 } from "uuid"; const router = Router(); // POST /api/v1/id-upload/token — Generate a one-time upload token // Called when client clicks "Upload from Phone (QR)" on the order form router.post("/api/v1/id-upload/token", async (req, res) => { try { const { customer_email, customer_name, order_reference } = req.body ?? {}; if (!customer_email) { res.status(400).json({ error: "Email is required." }); return; } const token = uuidv4(); const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours await pool.query( `INSERT INTO id_upload_tokens (token, customer_email, customer_name, order_reference, expires_at) VALUES ($1, $2, $3, $4, $5)`, [token, customer_email, customer_name || null, order_reference || null, expiresAt], ); const uploadUrl = `${req.protocol}://${req.get("host")?.replace(/:\d+$/, "")}:4322/upload/id?token=${token}`; // In production: https://performancewest.net/upload/id?token=${token} res.status(201).json({ token, upload_url: uploadUrl, expires_at: expiresAt.toISOString(), }); } catch (err) { console.error("[id-upload] Token creation error:", err); res.status(500).json({ error: "Could not create upload token." }); } }); // POST /api/v1/id-upload/:token — Upload ID images (multipart/form-data) // Called from both desktop file picker and mobile camera upload page router.post("/api/v1/id-upload/:token", async (req, res) => { try { const { token } = req.params; // Validate token const tokenResult = await pool.query( "SELECT * FROM id_upload_tokens WHERE token = $1", [token], ); if (tokenResult.rows.length === 0) { res.status(404).json({ error: "Invalid upload token." }); return; } const tokenData = tokenResult.rows[0]; if (tokenData.used) { res.status(410).json({ error: "This upload link has already been used." }); return; } if (new Date(tokenData.expires_at) < new Date()) { res.status(410).json({ error: "This upload link has expired. Please request a new one." }); return; } // For now, store a placeholder — actual file upload handling requires // multer middleware + MinIO upload (configured at deployment) const updates: string[] = []; const paths: Record = tokenData.minio_paths || {}; // Check what was uploaded (multipart fields: front, back) if (req.body.front_uploaded) { updates.push("front_uploaded = TRUE"); paths.front = `identity-docs/${token}/front.jpg`; } if (req.body.back_uploaded) { updates.push("back_uploaded = TRUE"); paths.back = `identity-docs/${token}/back.jpg`; } const bothDone = (tokenData.front_uploaded || req.body.front_uploaded) && (tokenData.back_uploaded || req.body.back_uploaded); if (bothDone) { updates.push("used = TRUE"); } if (updates.length > 0) { await pool.query( `UPDATE id_upload_tokens SET ${updates.join(", ")}, minio_paths = $1 WHERE token = $2`, [JSON.stringify(paths), token], ); } res.json({ success: true, front_uploaded: tokenData.front_uploaded || !!req.body.front_uploaded, back_uploaded: tokenData.back_uploaded || !!req.body.back_uploaded, complete: bothDone, }); } catch (err) { console.error("[id-upload] Upload error:", err); res.status(500).json({ error: "Upload failed." }); } }); // GET /api/v1/id-upload/:token/status — Check upload status (for desktop polling) router.get("/api/v1/id-upload/:token/status", async (req, res) => { try { const result = await pool.query( "SELECT front_uploaded, back_uploaded, used, expires_at FROM id_upload_tokens WHERE token = $1", [req.params.token], ); if (result.rows.length === 0) { res.status(404).json({ error: "Token not found." }); return; } const t = result.rows[0]; res.json({ front_uploaded: t.front_uploaded, back_uploaded: t.back_uploaded, complete: t.front_uploaded && t.back_uploaded, expired: new Date(t.expires_at) < new Date(), }); } catch (err) { res.status(500).json({ error: "Could not check status." }); } }); export default router;