Skip to content
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 53 additions & 7 deletions Parse-Dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,11 @@ module.exports = function(config, options) {
// In-memory conversation storage (consider using Redis in future)
const conversations = new Map();

// Agent API endpoint for handling AI requests - scoped to specific app
app.post('/apps/:appId/agent', async (req, res) => {
// Agent API endpoint handler
async function agentHandler(req, res) {
try {
const authentication = req.user;

const { message, modelName, conversationId, permissions } = req.body || {};
const { appId } = req.params;

Expand All @@ -221,11 +223,39 @@ module.exports = function(config, options) {
}

// Find the app in the configuration
const app = config.apps.find(app => (app.appNameForURL || app.appName) === appId);
if (!app) {
const appConfig = config.apps.find(a => (a.appNameForURL || a.appName) === appId);
if (!appConfig) {
return res.status(404).json({ error: `App "${appId}" not found` });
}

// Cross-app access control — restrict to apps the authenticated user has access to
const appsUserHasAccess = authentication && authentication.appsUserHasAccessTo;
let isPerAppReadOnly = false;
if (appsUserHasAccess) {
const matchingAccess = appsUserHasAccess.find(access => access.appId === appConfig.appId);
if (!matchingAccess) {
return res.status(403).json({ error: 'Forbidden: you do not have access to this app' });
}
isPerAppReadOnly = !!matchingAccess.readOnly;
}

// Determine if the user is read-only (globally or per-app)
const isReadOnly = (authentication && authentication.isReadOnly) || isPerAppReadOnly;

// Build the app context — always shallow copy to avoid mutating the shared config
const appContext = { ...appConfig };
if (isReadOnly) {
if (!appConfig.readOnlyMasterKey) {
return res.status(400).json({ error: 'You need to provide a readOnlyMasterKey to use read-only features.' });
}
appContext.masterKey = appConfig.readOnlyMasterKey;
}

// Resolve function-typed masterKey (supports dynamic key rotation via ConfigKeyCache)
if (typeof appContext.masterKey === 'function') {
appContext.masterKey = await ConfigKeyCache.get(appContext.appId, 'masterKey', appContext.masterKeyTtl, appContext.masterKey);
}

// Find the requested model
const modelConfig = config.agent.models.find(model => model.name === modelName);
if (!modelConfig) {
Expand Down Expand Up @@ -258,8 +288,12 @@ module.exports = function(config, options) {
// Array to track database operations for this request
const operationLog = [];

// Read-only users: override client permissions to deny all write operations,
// preventing privilege escalation via self-authorized permissions in the request body
const effectivePermissions = isReadOnly ? {} : (permissions || {});

// Make request to OpenAI API with app context and conversation history
const response = await makeOpenAIRequest(message, model, apiKey, app, conversationHistory, operationLog, permissions);
const response = await makeOpenAIRequest(message, model, apiKey, appContext, conversationHistory, operationLog, effectivePermissions);

// Update conversation history with user message and AI response
conversationHistory.push(
Expand All @@ -280,7 +314,7 @@ module.exports = function(config, options) {
conversationId: finalConversationId,
debug: {
timestamp: new Date().toISOString(),
appId: app.appId,
appId: appContext.appId,
modelUsed: model,
operations: operationLog
}
Expand All @@ -291,7 +325,19 @@ module.exports = function(config, options) {
const errorMessage = error.message || 'Provider error';
res.status(500).json({ error: `Error: ${errorMessage}` });
}
});
}

// Agent API endpoint — middleware chain: auth check (401) → CSRF validation (403) → handler
app.post('/apps/:appId/agent',
(req, res, next) => {
if (users && (!req.user || !req.user.isAuthenticated)) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
},
Authentication.csrfProtection,
agentHandler
);

/**
* Database function tools for the AI agent
Expand Down
Loading