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:
parent
f60c5229ab
commit
7ef509c247
2 changed files with 102 additions and 43 deletions
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue