179 lines
5.7 KiB
JavaScript
179 lines
5.7 KiB
JavaScript
const {
|
|
userFromSession,
|
|
multiUserMode,
|
|
safeJsonParse,
|
|
} = require("../utils/http");
|
|
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
|
const {
|
|
flexUserRoleValid,
|
|
ROLES,
|
|
} = require("../utils/middleware/multiUserProtected");
|
|
const { WorkspaceChats } = require("../models/workspaceChats");
|
|
const { Workspace } = require("../models/workspace");
|
|
const { ScheduledJobRun } = require("../models/scheduledJobRun");
|
|
const createFilesLib = require("../utils/agents/aibitat/plugins/create-files/lib");
|
|
const { Telemetry } = require("../models/telemetry");
|
|
|
|
/**
|
|
* Endpoints for serving agent-generated files (PPTX, etc.) with authentication
|
|
* and ownership validation.
|
|
*/
|
|
function agentFileServerEndpoints(app) {
|
|
if (!app) return;
|
|
|
|
/**
|
|
* Download a generated file by its storage filename.
|
|
* Validates that the requesting user has access to the workspace
|
|
* where the file was generated.
|
|
*/
|
|
app.get(
|
|
"/agent-skills/generated-files/:filename",
|
|
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
|
async (request, response) => {
|
|
try {
|
|
const user = await userFromSession(request, response);
|
|
const { filename } = request.params;
|
|
if (!filename)
|
|
return response.status(400).json({ error: "Filename is required" });
|
|
|
|
// Validate filename format
|
|
const parsed = createFilesLib.parseFilename(filename);
|
|
if (!parsed) {
|
|
return response
|
|
.status(400)
|
|
.json({ error: "Invalid filename format" });
|
|
}
|
|
|
|
// Find a chat or scheduled job run that references this file
|
|
const fileSource = await findFileSource(filename, {
|
|
user,
|
|
isMultiUser: multiUserMode(response),
|
|
});
|
|
|
|
if (!fileSource) {
|
|
return response.status(404).json({
|
|
error: "File not found or access denied",
|
|
});
|
|
}
|
|
|
|
// Retrieve the file from storage
|
|
const fileData = await createFilesLib.getGeneratedFile(filename);
|
|
if (!fileData) {
|
|
return response
|
|
.status(404)
|
|
.json({ error: "File not found in storage" });
|
|
}
|
|
|
|
// Get mime type and set headers for download
|
|
const mimeType = createFilesLib.getMimeType(`.${parsed.extension}`);
|
|
const safeFilename = createFilesLib.sanitizeFilenameForHeader(
|
|
fileSource.displayFilename || filename
|
|
);
|
|
response.setHeader("Content-Type", mimeType);
|
|
response.setHeader(
|
|
"Content-Disposition",
|
|
`attachment; filename="${safeFilename}"`
|
|
);
|
|
response.setHeader("Content-Length", fileData.buffer.length);
|
|
response.send(fileData.buffer);
|
|
Telemetry.sendTelemetry("agent_generated_file_downloaded", {
|
|
type: mimeType,
|
|
}).catch(() => {});
|
|
return;
|
|
} catch (error) {
|
|
console.error("[agentFileServer] Download error:", error.message);
|
|
return response.status(500).json({ error: "Failed to download file" });
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Locates the source record (a workspace chat or a scheduled job run) that
|
|
* references the given storage filename, and confirms the requester has access.
|
|
*
|
|
* Search order:
|
|
* 1. Workspace chats the user can access (per multi-user permissions).
|
|
* 2. Scheduled job runs — single-user only, so no per-user access check.
|
|
*
|
|
* @param {string} storageFilename
|
|
* @param {{ user: object|null, isMultiUser: boolean }} ctx
|
|
* @returns {Promise<{workspaceId: number|null, displayFilename: string}|null>}
|
|
*/
|
|
async function findFileSource(storageFilename, { user, isMultiUser }) {
|
|
try {
|
|
const fromChat = await findInWorkspaceChats(storageFilename, {
|
|
user,
|
|
isMultiUser,
|
|
});
|
|
if (fromChat) return fromChat;
|
|
|
|
if (isMultiUser) return null;
|
|
|
|
return await findInScheduledJobRuns(storageFilename);
|
|
} catch (error) {
|
|
console.error("[findFileSource] Error:", error.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Search workspace chats the user has access to. In single-user mode all
|
|
// workspaces are accessible; in multi-user mode only workspaces assigned to
|
|
// the user are. Returns the matching chat's workspace + display filename.
|
|
async function findInWorkspaceChats(storageFilename, { user, isMultiUser }) {
|
|
const workspaces =
|
|
isMultiUser && user
|
|
? await Workspace.whereWithUser(user)
|
|
: await Workspace.where();
|
|
|
|
const workspaceIds = workspaces.map((w) => w.id);
|
|
if (workspaceIds.length === 0) return null;
|
|
|
|
// DB-level filter so we don't load every chat into memory.
|
|
const chats = await WorkspaceChats.where({
|
|
workspaceId: { in: workspaceIds },
|
|
include: true,
|
|
response: { contains: storageFilename },
|
|
});
|
|
|
|
for (const chat of chats) {
|
|
const { outputs = [] } = safeJsonParse(chat.response, { outputs: [] });
|
|
const output = outputs.find(
|
|
(o) => o?.payload?.storageFilename === storageFilename
|
|
);
|
|
if (!output) continue;
|
|
return {
|
|
workspaceId: chat.workspaceId,
|
|
displayFilename:
|
|
output.payload.filename || output.payload.displayFilename,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Search completed scheduled job runs. Scheduled jobs are single-user only,
|
|
// so this skips access control. Returns the matching run's display filename.
|
|
async function findInScheduledJobRuns(storageFilename) {
|
|
const runs = await ScheduledJobRun.where({
|
|
status: "completed",
|
|
result: { contains: storageFilename },
|
|
});
|
|
|
|
for (const run of runs) {
|
|
const { outputs = [] } = safeJsonParse(run.result, { outputs: [] });
|
|
const output = outputs.find(
|
|
(o) => o?.payload?.storageFilename === storageFilename
|
|
);
|
|
if (!output) continue;
|
|
return {
|
|
workspaceId: null,
|
|
displayFilename: output.payload.filename || storageFilename,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
module.exports = { agentFileServerEndpoints };
|