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>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
129
api/src/routes/id-upload.ts
Normal file
129
api/src/routes/id-upload.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue