new-site/api/src/routes/id-upload.ts
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

129 lines
4.3 KiB
TypeScript

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<string, string> = 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;