diff --git a/.env.example b/.env.example index ad2181518..5f14d7939 100644 --- a/.env.example +++ b/.env.example @@ -51,6 +51,7 @@ TERMINUS_PROXY_HOST_URL= TERMINUS_ADMIN_ROOT_TOKEN= # Set this env to enable vault audit logs in the sidebar TERMINUS_RETRACED_PROJECT_ID= +TERMINUS_LLM_RETRACED_PROJECT_ID= # OpenTelemetry OTEL_EXPORTER_OTLP_METRICS_ENDPOINT= @@ -115,4 +116,11 @@ ENTERPRISE_ORY_PROJECT_ID= #OPENID_REQUEST_PROFILE_SCOPE=false # Uncomment below if you wish to forward the OpenID params (https://openid.net/specs/openid-connect-core-1_0-errata2.html#AuthRequest) to the OpenID IdP -#OPENID_REQUEST_FORWARD_PARAMS=true \ No newline at end of file +#OPENID_REQUEST_FORWARD_PARAMS=true + +TERMINUS_LLM_TENANT= +TERMINUS_LLM_PRODUCT= +TERMINUS_WRITE_API_KEY= +TERMINUS_READ_API_KEY= + +FEATURE_LLM_VAULT= diff --git a/Dockerfile b/Dockerfile index b4328282a..139005dab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG NODEJS_IMAGE=node:20.17-alpine3.19 +ARG NODEJS_IMAGE=node:20.18.1-alpine3.19 FROM --platform=$BUILDPLATFORM $NODEJS_IMAGE AS base # Install dependencies only when needed diff --git a/components/Navbar.tsx b/components/Navbar.tsx index 933fea32a..481233332 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -4,7 +4,7 @@ import { signOut } from 'next-auth/react'; import classNames from 'classnames'; import Link from 'next/link'; import { useTranslation } from 'next-i18next'; -import PowerIcon from '@heroicons/react/20/solid/PowerIcon'; +import { Power } from 'lucide-react'; export const Navbar = ({ session }: { session: Session | null }) => { const [isOpen, setIsOpen] = React.useState(false); @@ -49,7 +49,7 @@ export const Navbar = ({ session }: { session: Session | null }) => { data-testid='logout' id='user-menu-item-2' onClick={() => signOut()}> - + {t('sign_out')} diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 9bd727c78..9f765ff98 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -1,4 +1,4 @@ -import HomeIcon from '@heroicons/react/24/outline/HomeIcon'; +import { House, Settings } from 'lucide-react'; import Image from 'next/image'; import Link from 'next/link'; import classNames from 'classnames'; @@ -10,7 +10,8 @@ import SSOLogo from '@components/logo/SSO'; import DSyncLogo from '@components/logo/DSync'; import AuditLogsLogo from '@components/logo/AuditLogs'; import Vault from '@components/logo/Vault'; -import Cog8ToothIcon from '@heroicons/react/24/outline/Cog8ToothIcon'; +import { useCallback, useEffect } from 'react'; +import useFeatures from '@lib/ui/hooks/useFeatures'; type SidebarProps = { isOpen: boolean; @@ -22,19 +23,37 @@ type MenuItem = { href: string; text: string; active: boolean; + onClick?: () => void; icon?: any; items?: MenuItem[]; + current?: boolean; }; export const Sidebar = ({ isOpen, setIsOpen, branding }: SidebarProps) => { const { t } = useTranslation('common'); const { asPath } = useRouter(); - const menus = [ + const closeSidebar = useCallback(() => setIsOpen(false), [setIsOpen]); + + const features = useFeatures(); + + useEffect(() => { + function handleEscKey(e) { + if ((e as KeyboardEvent).key === 'Escape') { + closeSidebar(); + } + } + document.addEventListener('keydown', handleEscKey); + return () => { + document.removeEventListener('keydown', handleEscKey); + }; + }, [closeSidebar]); + + const menuItems = [ { href: '/admin/dashboard', text: t('dashboard'), - icon: HomeIcon, + icon: House, active: asPath.includes('/admin/dashboard'), }, { @@ -126,10 +145,31 @@ export const Sidebar = ({ isOpen, setIsOpen, branding }: SidebarProps) => { }, ], }, + features?.llmVault + ? { + href: '/admin/llm-vault/policies', + text: t('llm_vault'), + icon: Vault, + current: asPath.includes('llm-vault'), + active: asPath.includes('/admin/llm-vault'), + items: [ + { + href: '/admin/llm-vault/policies', + text: t('policies'), + active: asPath.includes('/admin/llm-vault/policies'), + }, + { + href: '/admin/llm-vault/audit-logs', + text: t('audit_logs'), + active: asPath.includes('/admin/llm-vault/audit-logs'), + }, + ], + } + : null, { href: '/admin/settings', text: t('settings'), - icon: Cog8ToothIcon, + icon: Settings, active: asPath.includes('/admin/settings'), items: [ { @@ -144,28 +184,30 @@ export const Sidebar = ({ isOpen, setIsOpen, branding }: SidebarProps) => { }, ], }, - ]; + ].filter((m): m is NonNullable => m !== null); + + const menus: MenuItem[] = menuItems; return ( <> {/* Sidebar for mobile */}
-
+
+
+
-
- -
- +
- {/* Sidebar for desktop */} -
-
-
+
+
+
+
-
+
@@ -204,11 +262,11 @@ export const Sidebar = ({ isOpen, setIsOpen, branding }: SidebarProps) => { const MenuItems = ({ menus }: { menus: MenuItem[] }) => { return ( -
); diff --git a/internal-ui/src/identity-federation/Edit.tsx b/internal-ui/src/identity-federation/Edit.tsx index 1c4a7bdcf..7cea8f110 100644 --- a/internal-ui/src/identity-federation/Edit.tsx +++ b/internal-ui/src/identity-federation/Edit.tsx @@ -4,8 +4,7 @@ import type { IdentityFederationApp } from '../types'; import TagsInput from 'react-tagsinput'; import { useTranslation } from 'next-i18next'; import { useFormik } from 'formik'; -import EyeIcon from '@heroicons/react/24/outline/EyeIcon'; -import EyeSlashIcon from '@heroicons/react/24/outline/EyeSlashIcon'; +import { Eye, EyeOff } from 'lucide-react'; import { Card } from '../shared'; import { defaultHeaders } from '../utils'; @@ -127,7 +126,7 @@ export const Edit = ({
{ e.preventDefault(); diff --git a/internal-ui/src/identity-federation/IdentityFederationApps.tsx b/internal-ui/src/identity-federation/IdentityFederationApps.tsx index d4f448cf3..b76df20d0 100644 --- a/internal-ui/src/identity-federation/IdentityFederationApps.tsx +++ b/internal-ui/src/identity-federation/IdentityFederationApps.tsx @@ -10,7 +10,7 @@ import { } from '../shared'; import { useTranslation } from 'next-i18next'; import type { IdentityFederationApp } from '../types'; -import PencilIcon from '@heroicons/react/24/outline/PencilIcon'; +import { Pencil } from 'lucide-react'; import { TableBodyType } from '../shared/Table'; import { pageLimit } from '../shared/Pagination'; import { useFetch, usePaginate } from '../hooks'; @@ -134,7 +134,7 @@ export const IdentityFederationApps = ({ { text: t('bui-shared-edit'), onClick: () => onEdit?.(apps.find((app) => app.id === row.id)!), - icon: , + icon: , }, ...actionCols.map((actionCol) => ({ text: actionCol.text, diff --git a/internal-ui/src/identity-federation/NewIdentityFederationApp.tsx b/internal-ui/src/identity-federation/NewIdentityFederationApp.tsx index 881348a0f..3a0ca45b5 100644 --- a/internal-ui/src/identity-federation/NewIdentityFederationApp.tsx +++ b/internal-ui/src/identity-federation/NewIdentityFederationApp.tsx @@ -3,7 +3,7 @@ import TagsInput from 'react-tagsinput'; import { Card, Button } from 'react-daisyui'; import { useTranslation } from 'next-i18next'; import type { IdentityFederationApp } from '../types'; -import QuestionMarkCircleIcon from '@heroicons/react/24/outline/QuestionMarkCircleIcon'; +import { CircleHelp } from 'lucide-react'; import { defaultHeaders } from '../utils'; import { AttributesMapping } from './AttributesMapping'; import { PageHeader } from '../shared'; @@ -199,7 +199,7 @@ export const NewIdentityFederationApp = ({ {t('bui-fs-generate-sp-entity-id')}
- +
diff --git a/internal-ui/src/setup-link/SetupLinks.tsx b/internal-ui/src/setup-link/SetupLinks.tsx index dfddebccd..74bec4279 100644 --- a/internal-ui/src/setup-link/SetupLinks.tsx +++ b/internal-ui/src/setup-link/SetupLinks.tsx @@ -1,9 +1,6 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'next-i18next'; -import EyeIcon from '@heroicons/react/24/outline/EyeIcon'; -import TrashIcon from '@heroicons/react/24/outline/TrashIcon'; -import ArrowPathIcon from '@heroicons/react/24/outline/ArrowPathIcon'; -import ClipboardDocumentIcon from '@heroicons/react/24/outline/ClipboardDocumentIcon'; +import { Eye, Trash, RefreshCw, Clipboard } from 'lucide-react'; import { addQueryParamsToPath, copyToClipboard } from '../utils'; import { TableBodyType } from '../shared/Table'; @@ -141,7 +138,7 @@ export const SetupLinks = ({ copyToClipboard(setupLink.url); onCopy(setupLink); }, - icon: , + icon: , }, { text: t('bui-shared-view'), @@ -149,7 +146,7 @@ export const SetupLinks = ({ setSetupLink(setupLink); setShowSetupLink(true); }, - icon: , + icon: , }, { text: t('bui-sl-regenerate'), @@ -157,7 +154,7 @@ export const SetupLinks = ({ setSetupLink(setupLink); setShowRegenModal(true); }, - icon: , + icon: , }, { destructive: true, @@ -166,7 +163,7 @@ export const SetupLinks = ({ setSetupLink(setupLink); setDelModal(true); }, - icon: , + icon: , }, ], } diff --git a/internal-ui/src/shared/Card.tsx b/internal-ui/src/shared/Card.tsx index 84d5e434f..57dde9886 100644 --- a/internal-ui/src/shared/Card.tsx +++ b/internal-ui/src/shared/Card.tsx @@ -2,7 +2,8 @@ import React from 'react'; const Card = ({ children, className }: { children: React.ReactNode; className?: string }) => { return ( -
+
{children}
); @@ -21,7 +22,7 @@ const Header = ({ children }: { children: React.ReactNode }) => { }; const Body = ({ children }: { children: React.ReactNode }) => { - return
{children}
; + return
{children}
; }; const Footer = ({ children }: { children: React.ReactNode }) => { diff --git a/internal-ui/src/shared/EmptyState.tsx b/internal-ui/src/shared/EmptyState.tsx index d1ca32074..b0c084311 100644 --- a/internal-ui/src/shared/EmptyState.tsx +++ b/internal-ui/src/shared/EmptyState.tsx @@ -1,9 +1,9 @@ -import InformationCircleIcon from '@heroicons/react/24/outline/InformationCircleIcon'; +import { Info } from 'lucide-react'; export const EmptyState = ({ title, description }: { title: string; description?: string | null }) => { return (
- +

{title}

{description &&

{description}

}
diff --git a/internal-ui/src/shared/InputWithCopyButton.tsx b/internal-ui/src/shared/InputWithCopyButton.tsx index 162f3f07b..2fe1f1f66 100644 --- a/internal-ui/src/shared/InputWithCopyButton.tsx +++ b/internal-ui/src/shared/InputWithCopyButton.tsx @@ -1,7 +1,7 @@ import { useTranslation } from 'next-i18next'; import { copyToClipboard } from '../utils'; import { IconButton } from './IconButton'; -import ClipboardDocumentIcon from '@heroicons/react/24/outline/ClipboardDocumentIcon'; +import { ClipboardCopy } from 'lucide-react'; export const CopyToClipboardButton = ({ text }: { text: string }) => { const { t } = useTranslation('common'); @@ -9,7 +9,7 @@ export const CopyToClipboardButton = ({ text }: { text: string }) => { return ( { copyToClipboard(text); diff --git a/internal-ui/src/shared/InputWithLabel.tsx b/internal-ui/src/shared/InputWithLabel.tsx new file mode 100644 index 000000000..74ae5e782 --- /dev/null +++ b/internal-ui/src/shared/InputWithLabel.tsx @@ -0,0 +1,21 @@ +import { InputHTMLAttributes } from 'react'; + +interface InputWithLabelProps extends InputHTMLAttributes { + label: string; +} +export const InputWithLabel = (props: InputWithLabelProps) => { + return ( + + ); +}; diff --git a/internal-ui/src/shared/LinkBack.tsx b/internal-ui/src/shared/LinkBack.tsx index b5e303b59..2a3e45aa6 100644 --- a/internal-ui/src/shared/LinkBack.tsx +++ b/internal-ui/src/shared/LinkBack.tsx @@ -1,4 +1,4 @@ -import ArrowLeftIcon from '@heroicons/react/24/outline/ArrowLeftIcon'; +import { ArrowLeft } from 'lucide-react'; import { useTranslation } from 'next-i18next'; import { ButtonOutline } from './ButtonOutline'; import { LinkOutline } from './LinkOutline'; @@ -16,7 +16,7 @@ export const LinkBack = ({ if (href) { return ( - + {t('back')} ); @@ -24,7 +24,7 @@ export const LinkBack = ({ if (onClick) { return ( - + {t('back')} ); diff --git a/internal-ui/src/shared/Loading.tsx b/internal-ui/src/shared/Loading.tsx index ff3fb8653..1d119aecf 100644 --- a/internal-ui/src/shared/Loading.tsx +++ b/internal-ui/src/shared/Loading.tsx @@ -1,8 +1,8 @@ -const Spinner = () => { +const Spinner = ({ className }: { className?: string }) => { return (
- + Loading...
diff --git a/internal-ui/src/shared/Pagination.tsx b/internal-ui/src/shared/Pagination.tsx index 53301d6d2..8833d00e3 100644 --- a/internal-ui/src/shared/Pagination.tsx +++ b/internal-ui/src/shared/Pagination.tsx @@ -1,6 +1,5 @@ import { useTranslation } from 'next-i18next'; -import ArrowLeftIcon from '@heroicons/react/24/outline/ArrowLeftIcon'; -import ArrowRightIcon from '@heroicons/react/24/outline/ArrowRightIcon'; +import { ArrowLeft, ArrowRight } from 'lucide-react'; import { ButtonOutline } from './ButtonOutline'; export const pageLimit = 50; @@ -30,7 +29,7 @@ export const Pagination = ({
@@ -38,7 +37,7 @@ export const Pagination = ({ diff --git a/internal-ui/src/shared/index.ts b/internal-ui/src/shared/index.ts index 39c009ccf..b706051e2 100644 --- a/internal-ui/src/shared/index.ts +++ b/internal-ui/src/shared/index.ts @@ -20,4 +20,5 @@ export { ButtonOutline } from './ButtonOutline'; export { Alert } from './Alert'; export { InputWithCopyButton, CopyToClipboardButton } from './InputWithCopyButton'; export { IconButton } from './IconButton'; +export { InputWithLabel } from './InputWithLabel'; export { PrismLoader } from './PrismLoader'; diff --git a/internal-ui/src/well-known/WellKnownURLs.tsx b/internal-ui/src/well-known/WellKnownURLs.tsx index caf5e6435..ca56fdff6 100644 --- a/internal-ui/src/well-known/WellKnownURLs.tsx +++ b/internal-ui/src/well-known/WellKnownURLs.tsx @@ -1,7 +1,7 @@ import Link from 'next/link'; import { useState } from 'react'; import { useTranslation } from 'next-i18next'; -import ArrowTopRightOnSquareIcon from '@heroicons/react/20/solid/ArrowTopRightOnSquareIcon'; +import { SquareArrowOutUpRight } from 'lucide-react'; export const WellKnownURLs = ({ jacksonUrl }: { jacksonUrl?: string }) => { const { t } = useTranslation('common'); @@ -156,7 +156,7 @@ const LinkCard = ({ href={href} target='_blank' rel='noreferrer'> - + {buttonText}
diff --git a/kustomize/overlays/demo/kustomization.yaml b/kustomize/overlays/demo/kustomization.yaml index b548d2a8b..55d85c74b 100644 --- a/kustomize/overlays/demo/kustomization.yaml +++ b/kustomize/overlays/demo/kustomization.yaml @@ -22,4 +22,4 @@ patches: images: - name: boxyhq/jackson - newTag: 1.27.1 + newTag: 1.31.0 diff --git a/lib/env.ts b/lib/env.ts index 61f35d568..ec23c6f4c 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -30,6 +30,21 @@ const terminus = { hostUrl: process.env.TERMINUS_PROXY_HOST_URL, adminToken: process.env.TERMINUS_ADMIN_ROOT_TOKEN, retracedProjectId: process.env.TERMINUS_RETRACED_PROJECT_ID, + llmRetracedProjectId: process.env.TERMINUS_LLM_RETRACED_PROJECT_ID, + apiKey: + process.env.TERMINUS_READ_API_KEY && process.env.TERMINUS_WRITE_API_KEY + ? { + read: process.env.TERMINUS_READ_API_KEY, + write: process.env.TERMINUS_WRITE_API_KEY, + } + : undefined, + llm: + process.env.TERMINUS_LLM_TENANT && process.env.TERMINUS_LLM_PRODUCT + ? { + product: process.env.TERMINUS_LLM_PRODUCT, + tenant: process.env.TERMINUS_LLM_TENANT, + } + : undefined, }; export const setupLinkExpiryDays = process.env.SETUP_LINK_EXPIRY_DAYS @@ -126,8 +141,11 @@ const adminPortalSSODefaults = { defaultRedirectUrl: `${externalUrl}/admin/auth/idp-login`, }; +const features = { llmVault: process.env.FEATURE_LLM_VAULT === 'true' }; + export { adminPortalSSODefaults }; export { retraced as retracedOptions }; export { terminus as terminusOptions }; export { apiKeys }; export { jacksonOptions }; +export { features }; diff --git a/lib/ui/hooks/useFeatures.ts b/lib/ui/hooks/useFeatures.ts new file mode 100644 index 000000000..3e53c1b72 --- /dev/null +++ b/lib/ui/hooks/useFeatures.ts @@ -0,0 +1,22 @@ +import { useEffect, useState } from 'react'; + +function useFeatures() { + const [features, setFeatures] = useState<{ [key: string]: boolean } | null>(null); + useEffect(() => { + const fetchLLMVaultFeatureStatus = async () => { + try { + const response = await fetch('/api/admin/features'); + const { data } = await response.json(); + setFeatures(data.features); + } catch (error) { + console.error('Error fetching list of features', error); + } + }; + + fetchLLMVaultFeatureStatus(); + }, []); + + return features; +} + +export default useFeatures; diff --git a/lib/zod/index.ts b/lib/zod/index.ts new file mode 100644 index 000000000..790aee9ab --- /dev/null +++ b/lib/zod/index.ts @@ -0,0 +1,12 @@ +import { ApiError } from '@lib/error'; +import z, { ZodType } from 'zod'; + +export const validateWithSchema = (schema: ZSchema, data: any) => { + const result = schema.safeParse(data); + + if (!result.success) { + throw new ApiError(`Validation Error: ${result.error.errors.map((e) => e.message)[0]}`, 422); + } + + return result.data as z.infer; +}; diff --git a/locales/en/common.json b/locales/en/common.json index e4f1b3b35..497f52e94 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -1,4 +1,28 @@ { + "llm_access_control_policy": "Access Control Policy", + "updated_at": "Updated At", + "llm_policy_deletion_success_toast": "The LLM Policy has been deleted successfully!", + "llm_policy_update_success_toast": "The LLM Policy has been updated successfully!", + "llm_policy_saved_success_toast": "The LLM Policy has been created successfully!", + "llm_pii_policy": "PII Policy", + "llm_no_policies_desc": "You have not created any LLM Policies yet.", + "llm_no_policies": "No Policies found.", + "llm_delete_policy_desc": "This action cannot be undone. This will permanently delete the LLM Policy.", + "llm_delete_policy_title": "Delete this LLM Policy", + "llm_edit_policy": "Edit LLM Policy", + "llm_new_policy": "Create New LLM Policy", + "llm_edit_policy_desc": "Update LLM policy to manage data protection rules", + "llm_new_policy_desc": "Configure a new LLM policy to manage data protection rules", + "select_policy": "Select a Policy", + "language": "Language", + "select_language": "Select a language", + "select_entities": "Select Entities to Monitor", + "select_all": "Select All", + "unselect_all": "Unselect All", + "collapse_all": "Collapse All", + "expand_all": "Expand All", + "no_entities_found": "No entities found matching", + "llm_vault": "LLM Vault", "apps": "Apps", "error_loading_page": "Unable to load this page. Maybe you don't have enough rights.", "documentation": "Documentation", diff --git a/npm/src/controller/utils.ts b/npm/src/controller/utils.ts index c2c53a3b9..22d24477b 100644 --- a/npm/src/controller/utils.ts +++ b/npm/src/controller/utils.ts @@ -25,6 +25,9 @@ export enum IndexNames { OIDCProviderClientID = 'OIDCProviderClientID', SSOClientID = 'SSOClientID', Product = 'product', + Tenant = 'tenant', + TenantProvider = 'tenantProvider', + TenantUser = 'tenantUser', // For Setup link Service = 'service', diff --git a/npm/src/index.ts b/npm/src/index.ts index 19d490ff6..41d5e3142 100644 --- a/npm/src/index.ts +++ b/npm/src/index.ts @@ -91,6 +91,8 @@ export const controllers = async ( const settingsStore = db.store('portal:settings'); const productStore = db.store('product:config'); const tracesStore = db.store('saml:tracer', tracesTTL); + const conversationStore = db.store('llm:conversation'); + const llmConfigStore = db.store('llm:config'); const ssoTraces = new SSOTraces({ tracesStore }); const eventController = new EventController({ opts }); diff --git a/npm/src/typings.ts b/npm/src/typings.ts index bbb01f25a..d8560bcf9 100644 --- a/npm/src/typings.ts +++ b/npm/src/typings.ts @@ -480,8 +480,13 @@ export interface JacksonOption { }; noAnalytics?: boolean; terminus?: { - host?: string; + hostUrl?: string; adminToken?: string; + apiKey?: { read: string; write: string }; + llm?: { + tenant: string; + product: string; + }; }; webhook?: Webhook; dsync?: { diff --git a/package-lock.json b/package-lock.json index 92413884f..ed8cafb4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@boxyhq/metrics": "0.2.9", "@boxyhq/react-ui": "3.3.45", "@boxyhq/saml-jackson": "file:npm", - "@heroicons/react": "2.2.0", + "@monaco-editor/react": "4.6.0", "@next/bundle-analyzer": "15.0.3", "@retracedhq/logs-viewer": "2.8.0", "@retracedhq/retraced": "0.7.17", @@ -28,6 +28,7 @@ "daisyui": "4.12.14", "formik": "2.4.6", "i18next": "24.0.5", + "lucide-react": "0.461.0", "medium-zoom": "1.1.0", "micromatch": "4.0.8", "next": "15.0.3", @@ -44,7 +45,8 @@ "react-tagsinput": "3.20.3", "remark-gfm": "4.0.0", "sharp": "0.33.5", - "swr": "2.2.5" + "swr": "2.2.5", + "zod": "3.23.8" }, "devDependencies": { "@playwright/test": "1.49.0", @@ -52,7 +54,7 @@ "@types/micromatch": "4.0.9", "@types/node": "20.12.12", "@types/prismjs": "1.26.5", - "@types/react": "18.3.2", + "@types/react": "18.3.3", "@typescript-eslint/eslint-plugin": "8.0.1", "@typescript-eslint/parser": "8.0.1", "autoprefixer": "10.4.20", @@ -103,9 +105,9 @@ }, "peerDependencies": { "@boxyhq/react-ui": ">=3.3.42", - "@heroicons/react": ">=2.1.1", "classnames": ">=2.5.1", "formik": ">=2.4.5", + "lucide-react": ">=0.461.0", "next": ">=14.1.0", "next-i18next": ">=13.3.0", "prismjs": ">=1.29.0", @@ -2609,15 +2611,6 @@ "node": ">=6" } }, - "node_modules/@heroicons/react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", - "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", - "license": "MIT", - "peerDependencies": { - "react": ">= 16 || ^19.0.0-rc" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3393,6 +3386,32 @@ "react": ">=16" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", @@ -7453,9 +7472,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz", - "integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==", + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -14935,6 +14954,15 @@ "url": "https://github.com/sponsors/wellwelwel" } }, + "node_modules/lucide-react": { + "version": "0.461.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.461.0.tgz", + "integrity": "sha512-Scpw3D/dV1bgVRC5Kh774RCm99z0iZpPv75M6kg7QL1lLvkQ1rmI1Sjjic1aGp1ULBwd7FokV6ry0g+d6pMB+w==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, "node_modules/macos-release": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-3.3.0.tgz", @@ -16456,6 +16484,13 @@ "obliterator": "^1.6.1" } }, + "node_modules/monaco-editor": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", + "license": "MIT", + "peer": true + }, "node_modules/mongodb": { "version": "5.9.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", @@ -21014,6 +21049,12 @@ "node": ">=8" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -24578,6 +24619,15 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 0b99eb74e..f6c320a46 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@boxyhq/metrics": "0.2.9", "@boxyhq/react-ui": "3.3.45", "@boxyhq/saml-jackson": "file:npm", - "@heroicons/react": "2.2.0", + "@monaco-editor/react": "4.6.0", "@next/bundle-analyzer": "15.0.3", "@retracedhq/logs-viewer": "2.8.0", "@retracedhq/retraced": "0.7.17", @@ -79,6 +79,7 @@ "daisyui": "4.12.14", "formik": "2.4.6", "i18next": "24.0.5", + "lucide-react": "0.461.0", "medium-zoom": "1.1.0", "micromatch": "4.0.8", "next": "15.0.3", @@ -95,7 +96,8 @@ "react-tagsinput": "3.20.3", "remark-gfm": "4.0.0", "sharp": "0.33.5", - "swr": "2.2.5" + "swr": "2.2.5", + "zod": "3.23.8" }, "devDependencies": { "@playwright/test": "1.49.0", @@ -103,7 +105,7 @@ "@types/micromatch": "4.0.9", "@types/node": "20.12.12", "@types/prismjs": "1.26.5", - "@types/react": "18.3.2", + "@types/react": "18.3.3", "@typescript-eslint/eslint-plugin": "8.0.1", "@typescript-eslint/parser": "8.0.1", "autoprefixer": "10.4.20", diff --git a/pages/admin/llm-vault/audit-logs.tsx b/pages/admin/llm-vault/audit-logs.tsx new file mode 100644 index 000000000..7212d8ecd --- /dev/null +++ b/pages/admin/llm-vault/audit-logs.tsx @@ -0,0 +1,51 @@ +import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import { retracedOptions, terminusOptions } from '@lib/env'; + +import { getToken } from '@lib/retraced'; +import type { Project } from 'types/retraced'; +import axios from 'axios'; +import jackson from '@lib/jackson'; +import { NextApiRequest, GetServerSideProps } from 'next'; + +export { default } from '@ee/terminus/pages/audit-logs'; + +export const getServerSideProps = (async ({ locale, req }) => { + const { checkLicense } = await jackson(); + + if (!terminusOptions.llmRetracedProjectId) { + return { + notFound: true, + }; + } else { + const token = await getToken(req as NextApiRequest); + try { + const { data } = await axios.get<{ project: Project }>( + `${retracedOptions?.hostUrl}/admin/v1/project/${terminusOptions.llmRetracedProjectId}`, + { + headers: { + Authorization: `id=${token.id} token=${token.token} admin_token=${retracedOptions.adminToken}`, + }, + } + ); + if (data.project.environments.length === 0) { + return { + notFound: true, + }; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (err) { + return { + notFound: true, + }; + } + } + + return { + props: { + ...(await serverSideTranslations(locale!, ['common'])), + host: retracedOptions.externalUrl, + projectId: terminusOptions.llmRetracedProjectId, + hasValidLicense: await checkLicense(), + }, + }; +}) satisfies GetServerSideProps; diff --git a/pages/admin/llm-vault/policies/edit/[product].tsx b/pages/admin/llm-vault/policies/edit/[product].tsx new file mode 100644 index 000000000..8b9a0c5ab --- /dev/null +++ b/pages/admin/llm-vault/policies/edit/[product].tsx @@ -0,0 +1,55 @@ +import type { GetServerSidePropsContext, NextPage } from 'next'; +import useSWR from 'swr'; +import { useRouter } from 'next/router'; +import { fetcher } from '@lib/ui/utils'; +import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import { Loading } from '@boxyhq/internal-ui'; +import { errorToast } from '@components/Toaster'; +import EditPolicyForm from '@components/terminus/policies/EditPolicyForm'; + +const EditPIIPolicy: NextPage = () => { + const router = useRouter(); + + const { product } = router.query as { product: string }; + + const { data, error, isLoading } = useSWR( + product ? `/api/admin/llm-vault/policies/${product}` : null, + fetcher, + { + revalidateOnFocus: false, + } + ); + + if (isLoading) { + return ; + } + + if (error) { + errorToast(error.message); + return null; + } + + if (!data) { + return null; + } + + return ( + + ); +}; + +export async function getServerSideProps({ locale }: GetServerSidePropsContext) { + return { + props: { + ...(locale ? await serverSideTranslations(locale, ['common']) : {}), + }, + }; +} + +export default EditPIIPolicy; diff --git a/pages/admin/llm-vault/policies/index.tsx b/pages/admin/llm-vault/policies/index.tsx new file mode 100644 index 000000000..e07ce506b --- /dev/null +++ b/pages/admin/llm-vault/policies/index.tsx @@ -0,0 +1,167 @@ +import type { NextPage } from 'next'; +import { Trash2, Pencil } from 'lucide-react'; +import { useTranslation } from 'next-i18next'; +import { Table, LinkPrimary, ConfirmationModal, EmptyState, Loading, Error } from '@boxyhq/internal-ui'; +import { useEffect, useState } from 'react'; +import router from 'next/router'; +import { errorToast, successToast } from '@components/Toaster'; +import { LanguageKey, SupportedLanguages } from '@components/terminus/policies/types'; +import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import { useFetch } from 'internal-ui/src/hooks'; + +export type policy = { + createdAt: string; + updatedAt: string; + product: string; + piiPolicy: string; + piiEntities: string; + language: string; +}; + +const Policies: NextPage = () => { + const { t } = useTranslation('common'); + + const { data, isLoading, error, refetch } = useFetch<{ data: Array }>({ + url: `/api/admin/llm-vault/policies`, + }); + + const [policies, setPolicies] = useState(data?.data); + const [delModalVisible, setDelModalVisible] = useState(false); + const [productToDelete, setProductToDelete] = useState(''); + + const getPolicies = async () => { + refetch(); + setPolicies(data?.data); + }; + + useEffect(() => { + setPolicies(data?.data); + }, [data]); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + const LANGUAGE_CODE_MAP: { [key: string]: LanguageKey } = Object.entries(SupportedLanguages).reduce( + (acc, [key, value]) => { + acc[value] = key as LanguageKey; + return acc; + }, + {} as { [key: string]: LanguageKey } + ); + + const getLanguageName = (language: string): string | undefined => { + return LANGUAGE_CODE_MAP[language]; + }; + + const deleteApp = async () => { + try { + const response = await fetch(`/api/admin/llm-vault/policies/${productToDelete}`, { + method: 'DELETE', + }); + + if (response.ok) { + successToast(t('llm_policy_deletion_success_toast')); + } + if (!response.ok) { + errorToast('Failed to delete piiPolicy'); + } + setDelModalVisible(false); + setProductToDelete(''); + getPolicies(); + } catch (error: any) { + errorToast(error); + } + }; + + return ( +
+
+

{t('policies')}

+ {t('llm_new_policy')} +
+ <> + {policies && policies?.length > 0 ? ( + { + return { + id: policy.product, + cells: [ + { + wrap: true, + text: policy.product, + }, + { + wrap: true, + text: policy.piiPolicy, + }, + { + wrap: true, + text: getLanguageName(policy.language), + }, + { + wrap: true, + text: new Date(policy.createdAt).toLocaleString(), + }, + { + wrap: true, + text: new Date(policy.updatedAt).toLocaleString(), + }, + { + actions: [ + { + text: t('bui-shared-edit'), + onClick: () => { + router.push(`/admin/llm-vault/policies/edit/${policy.product}`); + }, + icon: , + }, + { + text: t('bui-shared-delete'), + onClick: () => { + setDelModalVisible(true); + setProductToDelete(policy.product); + }, + icon: , + }, + ], + }, + ], + }; + })}>
+ ) : ( + + )} + deleteApp()} + onCancel={() => setDelModalVisible(false)} + /> + +
+ ); +}; + +export async function getServerSideProps({ locale }) { + return { + props: { + ...(await serverSideTranslations(locale, ['common'])), + }, + }; +} + +export default Policies; diff --git a/pages/admin/llm-vault/policies/new.tsx b/pages/admin/llm-vault/policies/new.tsx new file mode 100644 index 000000000..5eb29252b --- /dev/null +++ b/pages/admin/llm-vault/policies/new.tsx @@ -0,0 +1,16 @@ +import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import AddPolicyForm from '../../../../components/terminus/policies/AddPolicyForm'; + +const NewPolicy = () => { + return ; +}; + +export async function getServerSideProps({ locale }) { + return { + props: { + ...(await serverSideTranslations(locale, ['common'])), + }, + }; +} + +export default NewPolicy; diff --git a/pages/admin/retraced/projects/index.tsx b/pages/admin/retraced/projects/index.tsx index 82803bcec..193f40f47 100644 --- a/pages/admin/retraced/projects/index.tsx +++ b/pages/admin/retraced/projects/index.tsx @@ -1,6 +1,5 @@ import type { NextPage } from 'next'; -import DocumentMagnifyingGlassIcon from '@heroicons/react/24/outline/DocumentMagnifyingGlassIcon'; -import WrenchScrewdriverIcon from '@heroicons/react/24/outline/WrenchScrewdriverIcon'; +import { Wrench, FileSearch2 } from 'lucide-react'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { useProjects } from '@lib/ui/retraced'; import { useTranslation } from 'next-i18next'; @@ -62,14 +61,14 @@ const ProjectList: NextPage = () => { onClick: () => { router.push(`/admin/retraced/projects/${project.id}`); }, - icon: , + icon: , }, { text: t('view_events'), onClick: () => { router.push(`/admin/retraced/projects/${project.id}/events`); }, - icon: , + icon: , }, ], }, diff --git a/pages/admin/terminus/audit-logs.tsx b/pages/admin/terminus/audit-logs.tsx index ff75b0847..d20cf272e 100644 --- a/pages/admin/terminus/audit-logs.tsx +++ b/pages/admin/terminus/audit-logs.tsx @@ -1,113 +1,17 @@ -import type { NextPage, GetServerSideProps, NextApiRequest } from 'next'; -import dynamic from 'next/dynamic'; -import { useEffect, useState } from 'react'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { useProject, useGroups } from '@lib/ui/retraced'; -import { LinkBack, Loading, Error } from '@boxyhq/internal-ui'; -import { Select } from 'react-daisyui'; import { retracedOptions, terminusOptions } from '@lib/env'; -import { useTranslation } from 'next-i18next'; + import { getToken } from '@lib/retraced'; import type { Project } from 'types/retraced'; import axios from 'axios'; +import jackson from '@lib/jackson'; +import { NextApiRequest, GetServerSideProps } from 'next'; -const LogsViewer = dynamic(() => import('@components/retraced/LogsViewer'), { - ssr: false, -}); - -export interface Props { - host?: string; - projectId: string; -} - -const Events: NextPage = ({ host, projectId }: Props) => { - const { t } = useTranslation('common'); - - const [environment, setEnvironment] = useState(''); - const [group, setGroup] = useState(''); - - const { project, isLoading, isError } = useProject(projectId); - const { groups } = useGroups(projectId, environment); - - // Set the environment - useEffect(() => { - if (project) { - setEnvironment(project.environments[0]?.id); - } - }, [project]); - - // Set the group - useEffect(() => { - if (groups && groups.length > 0) { - setGroup(groups[0].group_id); - } - }, [groups]); - - if (isLoading) { - return ; - } - - if (isError) { - return ; - } - - const displayLogsViewer = project && environment && group; - - return ( -
- -
-

{project?.name}

-
-
-
- - {project ? ( - - ) : null} -
-
- - {groups ? ( - - ) : null} -
-
-
- {displayLogsViewer && ( - - )} -
-
- ); -}; +export { default } from '@ee/terminus/pages/audit-logs'; export const getServerSideProps = (async ({ locale, req }) => { + const { checkLicense } = await jackson(); + if (!terminusOptions.retracedProjectId) { return { notFound: true, @@ -141,8 +45,7 @@ export const getServerSideProps = (async ({ locale, req }) => { ...(await serverSideTranslations(locale!, ['common'])), host: retracedOptions.externalUrl, projectId: terminusOptions.retracedProjectId, + hasValidLicense: await checkLicense(), }, }; }) satisfies GetServerSideProps; - -export default Events; diff --git a/pages/admin/terminus/index.tsx b/pages/admin/terminus/index.tsx index ea877103c..08ac4a161 100644 --- a/pages/admin/terminus/index.tsx +++ b/pages/admin/terminus/index.tsx @@ -6,12 +6,19 @@ import '@components/terminus/blocks/customblocks'; import '@components/terminus/blocks/generator'; import { EmptyState } from '@boxyhq/internal-ui'; import { terminusOptions } from '@lib/env'; +import jackson from '@lib/jackson'; +import LicenseRequired from '@components/LicenseRequired'; export interface Props { host?: string; + hasValidLicense: boolean; } -const TerminusIndexPage: NextPage = ({ host }: Props) => { +const TerminusIndexPage: NextPage = ({ host, hasValidLicense }: Props) => { + if (!hasValidLicense) { + return ; + } + if (!host) { return ( = ({ host }: Props) => { }; export async function getServerSideProps({ locale }) { + const { checkLicense } = await jackson(); + return { props: { ...(await serverSideTranslations(locale, ['common'])), host: terminusOptions.hostUrl || null, + hasValidLicense: await checkLicense(), }, }; } diff --git a/pages/api/admin/features.ts b/pages/api/admin/features.ts new file mode 100644 index 000000000..8414060ba --- /dev/null +++ b/pages/api/admin/features.ts @@ -0,0 +1,15 @@ +import { defaultHandler } from '@lib/api'; +import { features } from '@lib/env'; +import { NextApiRequest, NextApiResponse } from 'next'; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + await defaultHandler(req, res, { + GET: handleGET, + }); +}; + +const handleGET = async (req: NextApiRequest, res: NextApiResponse) => { + res.json({ data: { features } }); +}; + +export default handler; diff --git a/pages/api/admin/llm-vault/policies/[product]/index.ts b/pages/api/admin/llm-vault/policies/[product]/index.ts new file mode 100644 index 000000000..da5d7be8c --- /dev/null +++ b/pages/api/admin/llm-vault/policies/[product]/index.ts @@ -0,0 +1,64 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import axios from 'axios'; +import { terminusOptions } from '@lib/env'; +import { defaultHandler } from '@lib/api'; + +async function handler(req: NextApiRequest, res: NextApiResponse) { + await defaultHandler(req, res, { + GET: getPolicy, + POST: savePolicy, + PUT: updatePolicy, + DELETE: deletePolicy, + }); +} + +const getTerminusUrl = (product: string) => { + return `${terminusOptions.hostUrl}/v1/manage/policies/${product}`; +}; + +// Utility function to handle API requests +const handleApiRequest = async ( + method: 'get' | 'post' | 'put' | 'delete', + req: NextApiRequest, + res: NextApiResponse +) => { + const { product } = req.query; + const url = getTerminusUrl(product as string); + + try { + const response = await axios({ + method, + url, + headers: { + Authorization: `api-key ${terminusOptions.adminToken}`, + }, + data: method !== 'get' ? req.body : undefined, + }); + + res.status(response.status).json({ + data: response.data, + error: null, + }); + } catch (error) { + if (axios.isAxiosError(error)) { + const status = error.response?.status || 500; + res.status(status).json({ + data: null, + error: error.message, + }); + } else { + console.error('Unexpected error:', error); + res.status(500).json({ + data: null, + error: 'An unexpected error occurred', + }); + } + } +}; + +const getPolicy = (req: NextApiRequest, res: NextApiResponse) => handleApiRequest('get', req, res); +const savePolicy = (req: NextApiRequest, res: NextApiResponse) => handleApiRequest('post', req, res); +const updatePolicy = (req: NextApiRequest, res: NextApiResponse) => handleApiRequest('put', req, res); +const deletePolicy = (req: NextApiRequest, res: NextApiResponse) => handleApiRequest('delete', req, res); + +export default handler; diff --git a/pages/api/admin/llm-vault/policies/entities.ts b/pages/api/admin/llm-vault/policies/entities.ts new file mode 100644 index 000000000..21e727dae --- /dev/null +++ b/pages/api/admin/llm-vault/policies/entities.ts @@ -0,0 +1,45 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import axios from 'axios'; +import { terminusOptions } from '@lib/env'; +import { defaultHandler } from '@lib/api'; + +async function handler(req: NextApiRequest, res: NextApiResponse) { + await defaultHandler(req, res, { + GET: getSupportedPIIEntities, + }); +} + +const getTerminusUrl = () => { + return `${terminusOptions.hostUrl}/v1/manage/supportedpiientities`; +}; + +const getSupportedPIIEntities = async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { data } = await axios.get(getTerminusUrl(), { + headers: { + Authorization: `api-key ${terminusOptions.adminToken}`, + }, + }); + + res.json({ + data, + error: null, + }); + } catch (error) { + if (axios.isAxiosError(error)) { + const status = error.response?.status; + res.status(status || 500).json({ + data: '', + error: error?.message, + }); + } else { + console.error('Unexpected error:', error); + res.status(500).json({ + data: null, + error: error, + }); + } + } +}; + +export default handler; diff --git a/pages/api/admin/llm-vault/policies/index.ts b/pages/api/admin/llm-vault/policies/index.ts new file mode 100644 index 000000000..68b4e28a5 --- /dev/null +++ b/pages/api/admin/llm-vault/policies/index.ts @@ -0,0 +1,41 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import axios from 'axios'; +import { terminusOptions } from '@lib/env'; +import { defaultHandler } from '@lib/api'; + +async function handler(req: NextApiRequest, res: NextApiResponse) { + await defaultHandler(req, res, { + GET: getPolicies, + }); +} + +const getPolicies = async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { data } = await axios.get(`${terminusOptions.hostUrl}/v1/manage/policies`, { + headers: { + Authorization: `api-key ${terminusOptions.adminToken}`, + }, + }); + + res.json({ + data, + error: null, + }); + } catch (error) { + if (axios.isAxiosError(error)) { + const status = error.response?.status; + res.status(status || 500).json({ + data: '', + error: error?.message, + }); + } else { + console.error('Unexpected error:', error); + res.status(500).json({ + data: null, + error: error, + }); + } + } +}; + +export default handler; diff --git a/pages/api/admin/terminus/models/[id]/index.ts b/pages/api/admin/terminus/models/[id]/index.ts index cd6f9d6fb..8a683c0ed 100644 --- a/pages/api/admin/terminus/models/[id]/index.ts +++ b/pages/api/admin/terminus/models/[id]/index.ts @@ -11,7 +11,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { } const getTerminusUrl = (id) => { - return `${terminusOptions.hostUrl}/v1/manage/${id}/model`; + return `${terminusOptions.hostUrl}/v1/manage/models/${id}`; }; const getModel = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index 553448f34..267c45108 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -1,5 +1,5 @@ import Adapter from '@lib/nextAuthAdapter'; -import NextAuth from 'next-auth'; +import NextAuth, { NextAuthOptions } from 'next-auth'; import EmailProvider from 'next-auth/providers/email'; import CredentialsProvider from 'next-auth/providers/credentials'; import BoxyHQSAMLProvider from 'next-auth/providers/boxyhq-saml'; @@ -8,7 +8,7 @@ import { validateEmailWithACL } from '@lib/utils'; import { jacksonOptions as env } from '@lib/env'; import { sessionName } from '@lib/constants'; -export default NextAuth({ +export const authOptions: NextAuthOptions = { theme: { colorScheme: 'light', }, @@ -161,6 +161,12 @@ export default NextAuth({ return validateEmailWithACL(user.email); }, + async session({ session, token }) { + if (session && token) { + session.user.id = token.sub; + } + return session; + }, }, pages: { signIn: '/admin/auth/login', @@ -168,4 +174,6 @@ export default NextAuth({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore adapter: Adapter(), -}); +}; + +export default NextAuth(authOptions); diff --git a/pages/setup/[token]/sso-connection/new.tsx b/pages/setup/[token]/sso-connection/new.tsx index 872e1f1a6..0064391cb 100644 --- a/pages/setup/[token]/sso-connection/new.tsx +++ b/pages/setup/[token]/sso-connection/new.tsx @@ -8,7 +8,7 @@ import { MDXRemote } from 'next-mdx-remote'; import { serialize } from 'next-mdx-remote/serialize'; import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import type { MDXRemoteSerializeResult } from 'next-mdx-remote'; -import ArrowsRightLeftIcon from '@heroicons/react/24/outline/ArrowsRightLeftIcon'; +import { ArrowRightLeft } from 'lucide-react'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { useTranslation } from 'next-i18next'; @@ -125,7 +125,7 @@ const NewConnection = ({

{heading}

{source && ( - + {t('change_identity_provider')} )} diff --git a/types/next-auth.d.ts b/types/next-auth.d.ts new file mode 100644 index 000000000..b85f246a1 --- /dev/null +++ b/types/next-auth.d.ts @@ -0,0 +1,9 @@ +import 'next-auth'; + +declare module 'next-auth' { + interface Session { + user: DefaultSession['user'] & { + id: string; + }; + } +}