/** * Photo ID upload — secure upload + storage + polling. * * POST /api/v1/id-upload/token — Generate upload token (from intake form) * POST /api/v1/id-upload/:token — Upload ID image (from phone or desktop) * GET /api/v1/id-upload/:token/status — Poll upload status (desktop polls this) */ import { Router, type Request, type Response } from "express"; import { pool } from "../db.js"; import { v4 as uuidv4 } from "uuid"; const router = Router(); const WORKER_URL = process.env.WORKER_URL || "http://workers:8090"; const MINIO_BUCKET = process.env.MINIO_BUCKET || "performancewest"; /** Ask the workers for a presigned MinIO URL (GET or PUT). */ async function presign(key: string, method: "GET" | "PUT", expires = 3600): Promise { if (!key) return null; try { const r = await fetch(`${WORKER_URL}/jobs/presign`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, expires, method }), }); if (!r.ok) return null; const data = (await r.json()) as { url?: string }; let url = data.url || null; if (url) { url = url.replace(/^http:\/\/minio:9000\//, "https://minio.performancewest.net/"); } return url; } catch { return null; } } /** Upload a buffer to MinIO via the workers. */ async function uploadToMinio(key: string, buffer: Buffer, contentType: string): Promise { try { const r = await fetch(`${WORKER_URL}/jobs/minio-upload`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, data: buffer.toString("base64"), content_type: contentType, bucket: MINIO_BUCKET, }), }); if (!r.ok) return false; const data = (await r.json()) as { success?: boolean }; return !!data.success; } catch { return false; } } // Ensure the id_upload_tokens table exists (async () => { try { await pool.query(` CREATE TABLE IF NOT EXISTS id_upload_tokens ( id SERIAL PRIMARY KEY, token TEXT NOT NULL UNIQUE, order_number TEXT, customer_email TEXT, customer_name TEXT, order_reference TEXT, front_uploaded BOOLEAN DEFAULT FALSE, back_uploaded BOOLEAN DEFAULT FALSE, used BOOLEAN DEFAULT FALSE, minio_paths JSONB DEFAULT '{}', expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ) `); } catch {} })(); // POST /api/v1/id-upload/token — Generate a one-time upload token router.post("/api/v1/id-upload/token", async (req: Request, res: Response) => { try { const { order_number, customer_email, customer_name } = req.body ?? {}; if (!order_number) { res.status(400).json({ error: "order_number 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, order_number, customer_email, customer_name, expires_at) VALUES ($1, $2, $3, $4, $5)`, [token, order_number, customer_email || null, customer_name || null, expiresAt], ); const domain = process.env.DOMAIN || "performancewest.net"; const uploadUrl = `https://${domain}/portal/upload-id/?order=${order_number}&upload_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 image router.post("/api/v1/id-upload/:token", async (req: Request, res: Response) => { 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] as Record; if (tokenData.used) { res.status(410).json({ error: "This upload link has already been used." }); return; } if (new Date(tokenData.expires_at as string) < new Date()) { res.status(410).json({ error: "This upload link has expired." }); return; } // Accept base64 image in JSON body const { image_data, filename, content_type } = req.body ?? {}; if (!image_data) { res.status(400).json({ error: "image_data (base64) is required." }); return; } // Decode base64 const base64Data = image_data.replace(/^data:[^;]+;base64,/, ""); const buffer = Buffer.from(base64Data, "base64"); // Store in MinIO via workers const ext = (content_type || "image/jpeg").split("/")[1] || "jpg"; const minioPath = `identity-docs/${tokenData.order_number}/${token}.${ext}`; const paths: Record = (tokenData.minio_paths as Record) || {}; const uploaded = await uploadToMinio(minioPath, buffer, content_type || "image/jpeg"); if (uploaded) { paths.front = minioPath; console.log(`[id-upload] Stored ${minioPath} (${buffer.length} bytes)`); } else { console.error("[id-upload] MinIO upload failed for", minioPath); res.status(500).json({ error: "Upload failed — please try again, or upload the file from your computer instead." }); return; } // Update token await pool.query( `UPDATE id_upload_tokens SET front_uploaded = TRUE, used = TRUE, minio_paths = $1 WHERE token = $2`, [JSON.stringify(paths), token], ); // Also update the compliance order's intake_data with photo flag if (tokenData.order_number) { await pool.query( `UPDATE compliance_orders SET intake_data = jsonb_set( COALESCE(intake_data, '{}'::jsonb), '{photo_id_uploaded}', 'true'::jsonb ) WHERE order_number = $1`, [tokenData.order_number], ).catch(() => {}); } // Generate a presigned thumbnail URL for the browser to display let thumbnailUrl = ""; if (uploaded) { thumbnailUrl = (await presign(minioPath, "GET", 3600)) || ""; } // Kick off async Ollama ID validation (non-blocking) validateIdWithOllama(base64Data, tokenData.order_number as string, token).catch(() => {}); res.json({ success: true, uploaded: true, minio_path: minioPath, thumbnail_url: thumbnailUrl, }); } 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 (desktop polling) router.get("/api/v1/id-upload/:token/status", async (req: Request, res: Response) => { try { const result = await pool.query( "SELECT front_uploaded, minio_paths, order_number, 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] as Record; const paths = (t.minio_paths as Record) || {}; // Generate thumbnail URL if uploaded let thumbnailUrl = ""; if (t.front_uploaded && paths.front && !paths.front.startsWith("base64")) { thumbnailUrl = (await presign(paths.front, "GET", 3600)) || ""; } res.json({ uploaded: !!t.front_uploaded, thumbnail_url: thumbnailUrl || null, expired: new Date(t.expires_at as string) < new Date(), }); } catch (err) { res.status(500).json({ error: "Could not check status." }); } }); // ── Ollama Vision ID Validation ──────────────────────────────────── const OLLAMA_URL = process.env.OLLAMA_URL || "http://ollama:11434"; const OLLAMA_MODEL = process.env.OLLAMA_VISION_MODEL || "minicpm-v:8b"; async function validateIdWithOllama( base64Image: string, orderNumber: string, token: string, ): Promise { try { const prompt = `Analyze this image. Is this a government-issued photo ID (driver's license, passport, state ID, or military ID)? Reply in this exact JSON format: {"is_id": true/false, "id_type": "driver_license/passport/state_id/military_id/unknown", "readable": true/false, "issues": ["list of issues if any"], "confidence": 0.0-1.0} Rules: - "is_id" = true only if this clearly shows a government photo ID - "readable" = true only if text on the ID appears legible - List issues like "blurry", "too dark", "partial/cropped", "glare", "wrong document" - Be strict: a photo of a credit card, insurance card, or random document is NOT an ID`; const resp = await fetch(`${OLLAMA_URL}/api/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: OLLAMA_MODEL, prompt, images: [base64Image.substring(0, 500000)], // Cap at ~375KB decoded stream: false, }), }); if (!resp.ok) { console.warn("[id-validate] Ollama returned", resp.status); return; } const data = (await resp.json()) as { response?: string }; const raw = data.response || ""; console.log("[id-validate] Ollama response:", raw.substring(0, 300)); // Parse JSON from response const jsonMatch = raw.match(/\{[\s\S]*?\}/); if (!jsonMatch) return; let validation: Record; try { validation = JSON.parse(jsonMatch[0]); } catch { return; } // Store validation result in the token record await pool.query( `UPDATE id_upload_tokens SET minio_paths = jsonb_set( COALESCE(minio_paths, '{}'::jsonb), '{validation}', $1::jsonb ) WHERE token = $2`, [JSON.stringify(validation), token], ).catch(() => {}); // If it's clearly not an ID, flag it in the order if (validation.is_id === false) { await pool.query( `UPDATE compliance_orders SET intake_data = jsonb_set( COALESCE(intake_data, '{}'::jsonb), '{photo_id_validation}', $1::jsonb ) WHERE order_number = $2`, [JSON.stringify({ valid: false, reason: "Uploaded image does not appear to be a government-issued photo ID", issues: validation.issues || [], checked_at: new Date().toISOString(), }), orderNumber], ).catch(() => {}); console.warn(`[id-validate] Order ${orderNumber}: uploaded image is NOT a valid ID`); } else { console.log(`[id-validate] Order ${orderNumber}: valid ${validation.id_type}, readable=${validation.readable}`); } } catch (err) { console.warn("[id-validate] Ollama validation failed:", err); } } export default router;