3 * @description Async agent installer build job management. Creates background build jobs for custom agent installers,
4 * tracks job status, and provides download URLs with 24-hour expiration. Jobs are queued in Redis for
5 * processing by builder service (BUILDER_URL).
6 * @module routes/downloadJob
10 * @apiDefine DownloadJobGroup Agent Installer Build Jobs
11 * Agent installer async build job management
14// backend/routes/downloadJob.js
15const express = require("express");
16const authenticateToken = require('../middleware/auth');
17const axios = require("axios");
18const { randomUUID } = require("crypto");
19const Redis = require("ioredis");
21const router = express.Router();
23// Builder URL (internal to your server)
24const BUILDER_URL = process.env.BUILDER_URL || "http://127.0.0.1:3001";
27 * @api {post} /api/download-job Create Agent Installer Build Job
28 * @apiName CreateDownloadJob
29 * @apiGroup DownloadJobGroup
30 * @apiDescription Creates an async background job to build a custom agent installer for a specific tenant/customer.
31 * Job is queued in Redis list 'builder:jobs' for processing by builder service.
32 * Returns jobId and URLs for status polling and download. Job expires after 24 hours.
33 * @apiParam {string} tenantId Tenant ID (must match authenticated user's tenantId)
34 * @apiParam {string} [customerId] Customer ID for installer customization
35 * @apiParam {string} os Target OS (e.g., 'windows', 'linux', 'macos')
36 * @apiSuccess {string} jobId UUID for the build job
37 * @apiSuccess {string} statusUrl URL to poll job status
38 * @apiSuccess {string} downloadUrl URL to download completed installer
39 * @apiError 400 Missing required fields (tenantId, os)
40 * @apiError 403 Forbidden if tenantId doesn't match authenticated user
41 * @apiError 500 Redis enqueue failure or server error
42 * @apiExample {curl} Example:
43 * curl -X POST https://api.example.com/api/download-job \
44 * -H "Authorization: Bearer YOUR_TOKEN" \
45 * -H "Content-Type: application/json" \
46 * -d '{"tenantId":"123", "customerId":"cust-456", "os":"windows"}'
47 * @apiExample {json} Success-Response (200):
49 * "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
50 * "statusUrl": "/api/download-job/a1b2c3d4-e5f6-7890-abcd-ef1234567890/status",
51 * "downloadUrl": "/api/download-job/a1b2c3d4-e5f6-7890-abcd-ef1234567890/download"
54// ------------------------------------------------------
55// POST /api/download-job
56// ------------------------------------------------------
57router.post("/download-job", authenticateToken, async (req, res) => {
59 const { tenantId, customerId, os } = req.body;
60 // Assume req.user.tenantId is set by authenticateToken middleware
61 const userTenantId = req.user && req.user.tenantId;
63 return res.status(400).json({ error: "Missing tenantId or os" });
64 if (!userTenantId || tenantId !== userTenantId) {
65 return res.status(403).json({ error: "Forbidden: tenantId mismatch" });
67 // Create a jobId (UUID)
68 const jobId = randomUUID();
70 // Enqueue job in Redis (BullMQ or simple list)
71 const redisConfig = require('../config/redis');
72 console.log(`[downloadJob] Using Redis: ${redisConfig.host}:${redisConfig.port}`);
73 const redis = new Redis(redisConfig);
75 // Add createdAt timestamp for cleanup retention
76 await redis.lpush('builder:jobs', JSON.stringify({ jobId, tenantId, customerId, os, createdAt: Date.now() }));
77 // Immediately create job hash for status endpoint
78 await redis.hset(`builder:job:${jobId}`, { status: 'queued', tenantId, customerId, os, createdAt: Date.now() });
79 console.log(`[downloadJob] Job created in Redis: ${jobId}`);
81 // Log full error and suggest fix for WRONGTYPE
82 console.error("[downloadJob] Redis error:", redisErr);
83 if (redisErr.message && redisErr.message.includes('WRONGTYPE')) {
84 console.error("[downloadJob] Redis key 'builder:jobs' is not a list. You may need to delete or rename this key: redis-cli DEL builder:jobs");
87 return res.status(500).json({ error: "Failed to enqueue job in Redis", details: redisErr.message });
91 // Return URLs immediately
94 statusUrl: `/api/download-job/${jobId}/status`,
95 downloadUrl: `/api/download-job/${jobId}/download`
99 console.error("[downloadJob] error:", err);
100 return res.status(500).json({ error: "Failed to create build job", details: err.message });
105 * @api {get} /api/download-job/:jobId/status Get Build Job Status
106 * @apiName GetDownloadJobStatus
107 * @apiGroup DownloadJobGroup
108 * @apiDescription Retrieves current status of an agent installer build job. Validates job exists and is not expired (24h).
109 * Proxies status from builder service. No authentication required (jobId is UUID secret).
110 * @apiParam {string} jobId UUID of the build job
111 * @apiSuccess {string} status Job status (e.g., 'queued', 'building', 'completed', 'failed')
112 * @apiSuccess {object} [data] Additional status data from builder service
113 * @apiError 404 Job not found in Redis
114 * @apiError 403 Job expired (older than 24 hours)
115 * @apiExample {curl} Example:
116 * curl https://api.example.com/api/download-job/a1b2c3d4-e5f6-7890-abcd-ef1234567890/status
117 * @apiExample {json} Success-Response (200):
119 * "status": "completed",
121 * "completedAt": 1672531200000
124// ------------------------------------------------------
125// GET /api/download-job/:jobId/status
126// ------------------------------------------------------
127router.get("/download-job/:jobId/status", async (req, res) => {
129 const jobId = req.params.jobId;
130 const redis = new Redis(require('../config/redis'));
131 const jobData = await redis.hgetall(`builder:job:${jobId}`);
133 // Check job exists and is not expired (24h)
134 if (!jobData || Object.keys(jobData).length === 0) {
135 return res.status(404).json({ error: "Job not found" });
137 const createdAt = Number(jobData.createdAt);
138 if (!createdAt || Date.now() - createdAt > 24 * 3600 * 1000) {
139 return res.status(403).json({ error: "Job expired" });
141 // If valid, proxy status from builder
142 const response = await axios.get(`${BUILDER_URL}/builder/status/${jobId}`);
143 return res.json(response.data);
145 return res.status(404).json({ error: "Job not found" });
150 * @api {get} /api/download-job/:jobId/download Download Agent Installer
151 * @apiName DownloadAgentInstaller
152 * @apiGroup DownloadJobGroup
153 * @apiDescription Downloads completed agent installer binary. Validates job exists and is not expired (24h).
154 * Proxies download from builder service as attachment stream. No authentication required (jobId is UUID secret).
155 * @apiParam {string} jobId UUID of the completed build job
156 * @apiSuccess (200) {Binary} file Agent installer binary
157 * @apiHeader {string} Content-Disposition attachment; filename=EverydayTechAgent_Installer.exe
158 * @apiError 404 Installer not found (job not found or build failed)
159 * @apiError 403 Job expired (older than 24 hours)
160 * @apiExample {curl} Example:
161 * curl -O -J https://api.example.com/api/download-job/a1b2c3d4-e5f6-7890-abcd-ef1234567890/download
163// ------------------------------------------------------
164// GET /api/download-job/:jobId/download
165// ------------------------------------------------------
166router.get("/download-job/:jobId/download", async (req, res) => {
168 const jobId = req.params.jobId;
169 const redis = new Redis(require('../config/redis'));
170 const jobData = await redis.hgetall(`builder:job:${jobId}`);
172 // Check job exists and is not expired (24h)
173 if (!jobData || Object.keys(jobData).length === 0) {
174 return res.status(404).json({ error: "Installer not found" });
176 const createdAt = Number(jobData.createdAt);
177 if (!createdAt || Date.now() - createdAt > 24 * 3600 * 1000) {
178 return res.status(403).json({ error: "Job expired" });
180 // If valid, proxy download from builder
181 const stream = await axios({
182 url: `${BUILDER_URL}/builder/download/${jobId}`,
184 responseType: "stream"
187 "Content-Disposition",
188 "attachment; filename=EverydayTechAgent_Installer.exe"
190 stream.data.pipe(res);
192 console.error("[download] error:", err);
193 return res.status(404).json({ error: "Installer not found" });
197module.exports = router;