fix photo ID upload: use workers for MinIO storage + public presigned URLs

- id-upload.ts: replace broken direct minio import with workers presign/upload
- job_server.py: add minio-upload handler for API to store files via workers
- rewrite presigned URLs from internal minio:9000 to public minio.performancewest.net
- fixes: thumbnail not showing after phone upload, base64 fallback storage
This commit is contained in:
justin 2026-05-30 18:12:06 -05:00
parent f60c5229ab
commit 7ef509c247
2 changed files with 102 additions and 43 deletions

View file

@ -1,4 +1,3 @@
// @ts-nocheck — minio is optional, only available in workers container
/**
* Photo ID upload secure upload + storage + polling.
*
@ -13,32 +12,63 @@ import { v4 as uuidv4 } from "uuid";
const router = Router();
// MinIO storage for ID documents
const MINIO_ENDPOINT = process.env.MINIO_ENDPOINT || "minio";
const MINIO_PORT = parseInt(process.env.MINIO_PORT || "9000");
const MINIO_ACCESS = process.env.MINIO_ACCESS_KEY || "";
const MINIO_SECRET = process.env.MINIO_SECRET_KEY || "";
const WORKER_URL = process.env.WORKER_URL || "http://workers:8090";
const MINIO_BUCKET = process.env.MINIO_BUCKET || "performancewest";
let minioClient: any = null;
async function getMinio() {
if (minioClient) return minioClient;
/** Ask the workers for a presigned MinIO URL (GET or PUT). */
async function presign(key: string, method: "GET" | "PUT", expires = 3600): Promise<string | null> {
if (!key) return null;
try {
const minio = await eval('import("minio")') as { Client: any };
const { Client } = minio;
minioClient = new Client({
endPoint: MINIO_ENDPOINT,
port: MINIO_PORT,
useSSL: false,
accessKey: MINIO_ACCESS,
secretKey: MINIO_SECRET,
const r = await fetch(`${WORKER_URL}/jobs/presign`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key, expires, method }),
});
return minioClient;
if (!r.ok) return null;
const data = (await r.json()) as { url?: string };
let url = data.url || null;
// Rewrite internal minio:9000 to public HTTPS endpoint
if (url) {
url = url.replace(/^http:\/\/minio:9000\//, "https://minio.performancewest.net/");
}
return url;
} catch {
return null;
}
}
/** Upload a buffer to MinIO using a presigned PUT URL. */
async function uploadToMinio(key: string, buffer: Buffer, contentType: string): Promise<boolean> {
const putUrl = await presign(key, "PUT", 300);
if (!putUrl) return false;
try {
// The presigned PUT URL may have internal hostname — use the workers endpoint directly
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,
}),
});
return r.ok;
} catch {
// Fallback: try presigned PUT
try {
const resp = await fetch(putUrl.replace("https://minio.performancewest.net/", "http://minio:9000/"), {
method: "PUT",
headers: { "Content-Type": contentType },
body: buffer,
});
return resp.ok;
} catch {
return false;
}
}
}
// Ensure the id_upload_tokens table exists
(async () => {
try {
@ -94,7 +124,6 @@ router.post("/api/v1/id-upload/token", async (req: Request, res: Response) => {
});
// POST /api/v1/id-upload/:token — Upload ID image
// Accepts raw body (base64 JSON) or multipart form data
router.post("/api/v1/id-upload/:token", async (req: Request, res: Response) => {
try {
const { token } = req.params;
@ -127,27 +156,22 @@ router.post("/api/v1/id-upload/:token", async (req: Request, res: Response) => {
}
// Decode base64
const base64Data = image_data.replace(/^data:image\/\w+;base64,/, "");
const base64Data = image_data.replace(/^data:[^;]+;base64,/, "");
const buffer = Buffer.from(base64Data, "base64");
// Store in MinIO
// 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<string, string> = (tokenData.minio_paths as Record<string, string>) || {};
const minio = await getMinio();
if (minio) {
await minio.putObject(MINIO_BUCKET, minioPath, buffer, buffer.length, {
"Content-Type": content_type || "image/jpeg",
"x-amz-meta-order": tokenData.order_number as string,
"x-amz-meta-type": "photo-id",
});
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 {
// Fallback: store base64 in DB (not ideal but works)
paths.front = `base64:${base64Data.substring(0, 50)}...`;
console.warn("[id-upload] MinIO not available — stored reference only");
// Fallback: store base64 reference in DB
paths.front = `base64-pending:${buffer.length}bytes`;
console.warn("[id-upload] MinIO upload failed — stored reference only");
}
// Update token
@ -167,12 +191,10 @@ router.post("/api/v1/id-upload/:token", async (req: Request, res: Response) => {
).catch(() => {});
}
// Generate a presigned thumbnail URL for the desktop to display
// Generate a presigned thumbnail URL for the browser to display
let thumbnailUrl = "";
if (minio) {
try {
thumbnailUrl = await minio.presignedGetObject(MINIO_BUCKET, minioPath, 3600);
} catch {}
if (uploaded) {
thumbnailUrl = (await presign(minioPath, "GET", 3600)) || "";
}
res.json({
@ -203,13 +225,8 @@ router.get("/api/v1/id-upload/:token/status", async (req: Request, res: Response
// Generate thumbnail URL if uploaded
let thumbnailUrl = "";
if (t.front_uploaded && paths.front) {
const minio = await getMinio();
if (minio) {
try {
thumbnailUrl = await minio.presignedGetObject(MINIO_BUCKET, paths.front, 3600);
} catch {}
}
if (t.front_uploaded && paths.front && !paths.front.startsWith("base64")) {
thumbnailUrl = (await presign(paths.front, "GET", 3600)) || "";
}
res.json({