diff --git a/api/server/index.js b/api/server/index.js index f88b2fd2581..87f144962fb 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -280,6 +280,7 @@ const startServer = async () => { app.use('/api/projects', routes.projects); app.use('/api/prompts', routes.prompts); app.use('/api/skills', routes.skills); + app.use('/api/artifact-apps', routes.artifactApps); app.use('/api/categories', routes.categories); app.use('/api/endpoints', routes.endpoints); app.use('/api/balance', routes.balance); diff --git a/api/server/middleware/accessResources/canAccessArtifactAppResource.js b/api/server/middleware/accessResources/canAccessArtifactAppResource.js new file mode 100644 index 00000000000..0df48493863 --- /dev/null +++ b/api/server/middleware/accessResources/canAccessArtifactAppResource.js @@ -0,0 +1,34 @@ +const { ResourceType } = require('librechat-data-provider'); +const { canAccessResource } = require('./canAccessResource'); +const { resolveArtifactAppId } = require('~/models'); + +/** + * Artifact-app-specific middleware factory that checks access permissions. + * Wraps the generic `canAccessResource` with the ARTIFACT_APP resource type and + * `resolveArtifactAppId` (custom `app_*` id → ObjectId) as the ID resolver. + * + * @param {Object} options + * @param {number} options.requiredPermission - Permission bit required (1=view, 2=edit, 4=delete, 8=share) + * @param {string} [options.resourceIdParam='id'] - Route parameter holding the artifact app id + * @returns {Function} Express middleware + */ +const canAccessArtifactAppResource = (options) => { + const { requiredPermission, resourceIdParam = 'id' } = options || {}; + + if (!requiredPermission || typeof requiredPermission !== 'number') { + throw new Error( + 'canAccessArtifactAppResource: requiredPermission is required and must be a number', + ); + } + + return canAccessResource({ + resourceType: ResourceType.ARTIFACT_APP, + requiredPermission, + resourceIdParam, + idResolver: (artifactAppId) => resolveArtifactAppId({ artifactAppId }), + }); +}; + +module.exports = { + canAccessArtifactAppResource, +}; diff --git a/api/server/middleware/accessResources/index.js b/api/server/middleware/accessResources/index.js index 31548411bac..6af901fade1 100644 --- a/api/server/middleware/accessResources/index.js +++ b/api/server/middleware/accessResources/index.js @@ -5,6 +5,7 @@ const { canAccessPromptViaGroup } = require('./canAccessPromptViaGroup'); const { canAccessPromptGroupResource } = require('./canAccessPromptGroupResource'); const { canAccessMCPServerResource } = require('./canAccessMCPServerResource'); const { canAccessSkillResource } = require('./canAccessSkillResource'); +const { canAccessArtifactAppResource } = require('./canAccessArtifactAppResource'); module.exports = { canAccessResource, @@ -14,4 +15,5 @@ module.exports = { canAccessPromptGroupResource, canAccessMCPServerResource, canAccessSkillResource, + canAccessArtifactAppResource, }; diff --git a/api/server/routes/artifactApps.js b/api/server/routes/artifactApps.js new file mode 100644 index 00000000000..1ce190e9e02 --- /dev/null +++ b/api/server/routes/artifactApps.js @@ -0,0 +1,102 @@ +const express = require('express'); +const { createArtifactAppHandlers } = require('@librechat/api'); +const { PermissionBits } = require('librechat-data-provider'); +const { + createArtifactAppWithVersion, + getArtifactAppByAppId, + resolveArtifactAppId, + listArtifactApps, + updateArtifactApp, + deleteArtifactApp, + getArtifactVersion, + listArtifactVersions, + createArtifactVersion, + releaseArtifactVersion, + activateArtifactVersion, + withdrawArtifactVersion, + recordAuditEntry, +} = require('~/models'); +const { requireJwtAuth, canAccessArtifactAppResource } = require('~/server/middleware'); +const { + findAccessibleResources, + grantPermission, +} = require('~/server/services/PermissionService'); +const configMiddleware = require('~/server/middleware/config/app'); + +const router = express.Router(); + +router.use(requireJwtAuth); +router.use(configMiddleware); + +const handlers = createArtifactAppHandlers({ + createArtifactAppWithVersion, + getArtifactAppByAppId, + resolveArtifactAppId, + listArtifactApps, + updateArtifactApp, + deleteArtifactApp, + getArtifactVersion, + listArtifactVersions, + createArtifactVersion, + releaseArtifactVersion, + activateArtifactVersion, + withdrawArtifactVersion, + findAccessibleResources, + grantPermission, + recordAuditEntry, +}); + +// Collection +router.get('/', handlers.list); +router.post('/', handlers.publish); + +// Single app +router.get( + '/:id', + canAccessArtifactAppResource({ requiredPermission: PermissionBits.VIEW }), + handlers.get, +); +router.patch( + '/:id', + canAccessArtifactAppResource({ requiredPermission: PermissionBits.EDIT }), + handlers.update, +); +router.delete( + '/:id', + canAccessArtifactAppResource({ requiredPermission: PermissionBits.DELETE }), + handlers.remove, +); + +// Versions +router.get( + '/:id/versions', + canAccessArtifactAppResource({ requiredPermission: PermissionBits.VIEW }), + handlers.listVersions, +); +router.post( + '/:id/versions', + canAccessArtifactAppResource({ requiredPermission: PermissionBits.EDIT }), + handlers.createVersion, +); +router.get( + '/:id/versions/:versionId', + canAccessArtifactAppResource({ requiredPermission: PermissionBits.VIEW }), + handlers.getVersion, +); +router.post( + '/:id/versions/:versionId/release', + canAccessArtifactAppResource({ requiredPermission: PermissionBits.EDIT }), + handlers.releaseVersion, +); +router.post( + '/:id/versions/:versionId/activate', + canAccessArtifactAppResource({ requiredPermission: PermissionBits.EDIT }), + handlers.activateVersion, +); +router.post( + '/:id/versions/:versionId/withdraw', + canAccessArtifactAppResource({ requiredPermission: PermissionBits.EDIT }), + handlers.withdrawVersion, +); + +module.exports = router; diff --git a/api/server/routes/index.js b/api/server/routes/index.js index ac2b38f579a..9491394ef9c 100644 --- a/api/server/routes/index.js +++ b/api/server/routes/index.js @@ -17,6 +17,7 @@ const presets = require('./presets'); const projects = require('./projects'); const prompts = require('./prompts'); const skills = require('./skills'); +const artifactApps = require('./artifactApps'); const balance = require('./balance'); const actions = require('./actions'); const apiKeys = require('./apiKeys'); @@ -66,6 +67,7 @@ module.exports = { prompts, projects, skills, + artifactApps, actions, presets, balance, diff --git a/client/src/components/ArtifactApps/AppRenderer.tsx b/client/src/components/ArtifactApps/AppRenderer.tsx new file mode 100644 index 00000000000..a64cb204020 --- /dev/null +++ b/client/src/components/ArtifactApps/AppRenderer.tsx @@ -0,0 +1,41 @@ +import { useRef } from 'react'; +import type { SandpackPreviewRef } from '@codesandbox/sandpack-react/unstyled'; +import type { TArtifactVersion } from 'librechat-data-provider'; +import type { Artifact } from '~/common'; +import { ArtifactPreview } from '~/components/Artifacts/ArtifactPreview'; +import useArtifactProps from '~/hooks/Artifacts/useArtifactProps'; +import { useGetStartupConfig } from '~/data-provider'; + +export default function AppRenderer({ + title, + version, +}: { + title: string; + version: TArtifactVersion; +}) { + const previewRef = useRef(null as unknown as SandpackPreviewRef); + const { data: startupConfig } = useGetStartupConfig(); + + const artifact: Artifact = { + id: version.artifactVersionId, + lastUpdateTime: Date.now(), + type: version.artifactType, + content: version.sourceSnapshot, + title, + }; + + const { files, fileKey, template, sharedProps } = useArtifactProps({ artifact }); + + return ( +
+ +
+ ); +} diff --git a/client/src/components/ArtifactApps/ArtifactAppsList.tsx b/client/src/components/ArtifactApps/ArtifactAppsList.tsx new file mode 100644 index 00000000000..1df227beaf1 --- /dev/null +++ b/client/src/components/ArtifactApps/ArtifactAppsList.tsx @@ -0,0 +1,105 @@ +import { useNavigate } from 'react-router-dom'; +import { Rocket, Lock, Globe, Users } from 'lucide-react'; +import { useListArtifactAppsQuery } from '~/data-provider'; +import { useLocalize } from '~/hooks'; +import type { TArtifactApp } from 'librechat-data-provider'; + +function visibilityIcon(v: TArtifactApp['visibility']) { + if (v === 'public') return