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({
|
||||
|
|
|
|||
|
|
@ -1569,6 +1569,47 @@ def handle_presign(payload: dict) -> dict:
|
|||
return {"error": str(exc)}
|
||||
|
||||
|
||||
def handle_minio_upload(payload: dict) -> dict:
|
||||
"""Upload a base64-encoded file to MinIO.
|
||||
|
||||
Payload: { key: str, data: str (base64), content_type: str, bucket: str }
|
||||
Returns: { success: bool, key: str, size: int }
|
||||
"""
|
||||
key = payload.get("key", "")
|
||||
data_b64 = payload.get("data", "")
|
||||
content_type = payload.get("content_type", "application/octet-stream")
|
||||
bucket = payload.get("bucket", os.getenv("MINIO_BUCKET", "performancewest"))
|
||||
|
||||
if not key or not data_b64:
|
||||
return {"error": "key and data required"}
|
||||
|
||||
try:
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from minio import Minio as _Minio
|
||||
|
||||
buf = base64.b64decode(data_b64)
|
||||
mc = _Minio(
|
||||
f"{os.getenv('MINIO_ENDPOINT', 'minio')}:{os.getenv('MINIO_PORT', '9000')}",
|
||||
access_key=os.getenv("MINIO_ACCESS_KEY", ""),
|
||||
secret_key=os.getenv("MINIO_SECRET_KEY", ""),
|
||||
secure=os.getenv("MINIO_SECURE", "false").lower() == "true",
|
||||
)
|
||||
if not mc.bucket_exists(bucket):
|
||||
mc.make_bucket(bucket)
|
||||
|
||||
mc.put_object(
|
||||
bucket, key, BytesIO(buf), len(buf),
|
||||
content_type=content_type,
|
||||
metadata={"x-amz-meta-type": "photo-id"},
|
||||
)
|
||||
LOG.info("[minio-upload] Stored %s (%d bytes)", key, len(buf))
|
||||
return {"success": True, "key": key, "size": len(buf)}
|
||||
except Exception as exc:
|
||||
LOG.error("[minio-upload] Failed to upload %s: %s", key, exc)
|
||||
return {"error": str(exc)}
|
||||
|
||||
|
||||
def handle_esign_completed(payload: dict) -> dict:
|
||||
"""Generic eSign completion callback — resume the service pipeline.
|
||||
|
||||
|
|
@ -1786,6 +1827,7 @@ JOB_HANDLERS = {
|
|||
"purchase_client_selections": handle_purchase_client_selections,
|
||||
# eSign / MinIO helpers
|
||||
"presign": handle_presign,
|
||||
"minio-upload": handle_minio_upload,
|
||||
"esign_completed": handle_esign_completed,
|
||||
"resume_crtc_pipeline": handle_resume_crtc_pipeline,
|
||||
# Compliance calendar renewal
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue