updates across the board
This commit is contained in:
parent
fc403142c2
commit
1152abd4a5
@ -11,6 +11,13 @@ JWT_SECRET=sdfj94mfm430f72m3487rdsjiy7834n9rnf934n8r3n490fn4u83fh894hr9nf0
|
|||||||
# SERVER_BASEPATH_API=v1/
|
# SERVER_BASEPATH_API=v1/
|
||||||
# TIMEZONE=Europe/Amsterdam
|
# TIMEZONE=Europe/Amsterdam
|
||||||
|
|
||||||
|
|
||||||
|
# Default Admin User
|
||||||
|
DEFAULT_ADMIN_USERNAME=admin
|
||||||
|
DEFAULT_ADMIN_EMAIL=darren@fusero.nl
|
||||||
|
DEFAULT_ADMIN_PASSWORD=admin123
|
||||||
|
|
||||||
|
|
||||||
FASTIFY_PORT=14000
|
FASTIFY_PORT=14000
|
||||||
|
|
||||||
# [ Database ]
|
# [ Database ]
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
frontend:
|
fusero-app-frontend:
|
||||||
container_name: fusero-frontend
|
container_name: fusero-app-frontend
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@ -11,13 +11,13 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- fusero-network
|
- fusero-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- fusero-app-boilerplate
|
- fusero-app-backend
|
||||||
|
|
||||||
frontend-dev:
|
fusero-app-frontend-dev:
|
||||||
container_name: fusero-frontend-dev
|
container_name: fusero-app-frontend-dev
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile.dev
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- '8080:8080'
|
- '8080:8080'
|
||||||
volumes:
|
volumes:
|
||||||
@ -29,11 +29,9 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- fusero-network
|
- fusero-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- fusero-app-boilerplate
|
- fusero-app-backend
|
||||||
|
|
||||||
fusero-app-boilerplate:
|
fusero-app-backend:
|
||||||
environment:
|
|
||||||
- POSTGRES_HOST=fusero-boilerplate-db
|
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@ -43,7 +41,7 @@ services:
|
|||||||
- '5000:14000'
|
- '5000:14000'
|
||||||
depends_on:
|
depends_on:
|
||||||
- fusero-boilerplate-db
|
- fusero-boilerplate-db
|
||||||
container_name: fusero-app-boilerplate
|
container_name: fusero-app-backend
|
||||||
networks:
|
networks:
|
||||||
- fusero-network
|
- fusero-network
|
||||||
|
|
||||||
@ -73,6 +71,21 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=test-db
|
- POSTGRES_DB=test-db
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: fusero-nginx
|
||||||
|
ports:
|
||||||
|
- '14001:80'
|
||||||
|
- '14443:443'
|
||||||
|
volumes:
|
||||||
|
- ./nginx/nginx.conf.prod:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
- ./nginx/certs:/etc/nginx/certs:ro
|
||||||
|
depends_on:
|
||||||
|
- fusero-app-frontend
|
||||||
|
- fusero-app-backend
|
||||||
|
networks:
|
||||||
|
- fusero-network
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
fusero_boilerplate_pgdata:
|
fusero_boilerplate_pgdata:
|
||||||
external: true
|
external: true
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="public/favicon/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="public/favicon/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
|
||||||
<link rel="manifest" href="public/favicon/site.webmanifest">
|
<link rel="manifest" href="/favicon/site.webmanifest">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Fusero</title>
|
<title>Fusero</title>
|
||||||
</head>
|
</head>
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name localhost;
|
server_name _;
|
||||||
|
|
||||||
location / {
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Proxy API requests to the backend
|
location /favicon/ {
|
||||||
location /api {
|
alias /usr/share/nginx/html/dist/favicon/;
|
||||||
proxy_pass http://backend:14000;
|
access_log off;
|
||||||
proxy_http_version 1.1;
|
expires max;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# DO NOT proxy /api here — let the global Nginx handle it
|
||||||
}
|
}
|
27
frontend/nginx.conf.bkup
Normal file
27
frontend/nginx.conf.bkup
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Serve favicon files
|
||||||
|
location /favicon/ {
|
||||||
|
alias /usr/share/nginx/html/dist/favicon/;
|
||||||
|
access_log off;
|
||||||
|
expires max;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy API requests to the backend
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://fusero-app-backend:14000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
}
|
33
frontend/package-lock.json
generated
33
frontend/package-lock.json
generated
@ -24,6 +24,7 @@
|
|||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.15.18",
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
@ -1696,6 +1697,16 @@
|
|||||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "22.15.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz",
|
||||||
|
"integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.12",
|
"version": "15.7.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
||||||
@ -5796,6 +5807,13 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unified": {
|
"node_modules/unified": {
|
||||||
"version": "11.0.5",
|
"version": "11.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
||||||
@ -7264,6 +7282,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="
|
||||||
},
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"version": "22.15.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz",
|
||||||
|
"integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/prop-types": {
|
"@types/prop-types": {
|
||||||
"version": "15.7.12",
|
"version": "15.7.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
||||||
@ -9969,6 +9996,12 @@
|
|||||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"unified": {
|
"unified": {
|
||||||
"version": "11.0.5",
|
"version": "11.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.15.18",
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { ModalConfig } from '../core/types';
|
import { ModalConfig } from '../core/types';
|
||||||
import { useModal } from '../context/ModalContext';
|
import { useModal } from '../context/ModalContext';
|
||||||
|
|
||||||
interface ModalProps extends ModalConfig {}
|
interface ModalProps extends Omit<ModalConfig, 'content'> {
|
||||||
|
content: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
export const Modal: React.FC<ModalProps> = ({
|
export const Modal: React.FC<ModalProps> = ({
|
||||||
id,
|
id,
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Modal } from './Modal';
|
import { Modal } from './Modal';
|
||||||
import { useModal } from '../context/ModalContext';
|
import { useModal } from '../context/ModalContext';
|
||||||
|
import { ModalConfig } from '../core/types';
|
||||||
|
|
||||||
|
interface ModalWithContent extends ModalConfig {
|
||||||
|
content: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
export const ModalRenderer: React.FC = () => {
|
export const ModalRenderer: React.FC = () => {
|
||||||
const { modals } = useModal();
|
const { modals } = useModal();
|
||||||
@ -10,7 +15,7 @@ export const ModalRenderer: React.FC = () => {
|
|||||||
{modals.map((modal) => (
|
{modals.map((modal) => (
|
||||||
<Modal
|
<Modal
|
||||||
key={modal.id}
|
key={modal.id}
|
||||||
{...modal}
|
{...modal as ModalWithContent}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Card } from 'primereact/card';
|
import { Card } from 'primereact/card';
|
||||||
|
import { Dialog } from 'primereact/dialog';
|
||||||
|
import { InputText } from 'primereact/inputtext';
|
||||||
|
import { InputTextarea } from 'primereact/inputtextarea';
|
||||||
|
import { FileUpload } from 'primereact/fileupload';
|
||||||
import Button from '../shared/components/_V2/Button';
|
import Button from '../shared/components/_V2/Button';
|
||||||
import Dropdown from '../shared/components/_V2/Dropdown';
|
import Dropdown from '../shared/components/_V2/Dropdown';
|
||||||
import Divider from '../shared/components/_V1/Divider';
|
import Divider from '../shared/components/_V1/Divider';
|
||||||
@ -12,10 +16,15 @@ import { useSystemStore } from '../state/stores/useSystemStore';
|
|||||||
export type SystemDropdownOption = {
|
export type SystemDropdownOption = {
|
||||||
id: number;
|
id: number;
|
||||||
label: string;
|
label: string;
|
||||||
|
name: string;
|
||||||
businessLabel: string;
|
businessLabel: string;
|
||||||
urlSlug: string;
|
urlSlug: string;
|
||||||
logo: string;
|
logo: string;
|
||||||
category: string;
|
category: string;
|
||||||
|
method?: string;
|
||||||
|
path?: string;
|
||||||
|
description?: string;
|
||||||
|
publicPath?: string;
|
||||||
enterprises?: Enterprise[];
|
enterprises?: Enterprise[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -30,44 +39,61 @@ export interface SystemSelectionModalProps {
|
|||||||
onLogoChange: (newLogoPath: string) => void;
|
onLogoChange: (newLogoPath: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: removed mocked put in db
|
|
||||||
const mockedSystems = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
label: 'CouncilAI',
|
|
||||||
businessLabel: 'CouncilAI',
|
|
||||||
urlSlug: 'council',
|
|
||||||
logo: fusero_logo,
|
|
||||||
category: 'Apps',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
label: 'FuseMind',
|
|
||||||
businessLabel: 'FuseMind (AI)',
|
|
||||||
urlSlug: 'fusemind',
|
|
||||||
logo: fusero_logo,
|
|
||||||
category: 'Apps',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
label: 'Canvas API',
|
|
||||||
businessLabel: 'Canvas API',
|
|
||||||
urlSlug: 'canvas',
|
|
||||||
logo: roc_logo,
|
|
||||||
category: 'Apps',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Add system default routes configuration
|
// Add system default routes configuration
|
||||||
const systemDefaultRoutes = {
|
const systemDefaultRoutes = {
|
||||||
council: 'chat',
|
council: 'chat',
|
||||||
fusemind: 'home',
|
fusemind: 'home',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const HARDCODED_SYSTEMS = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
label: 'Canvas API',
|
||||||
|
name: 'Canvas API',
|
||||||
|
businessLabel: 'Canvas',
|
||||||
|
urlSlug: 'canvas',
|
||||||
|
logo: roc_logo,
|
||||||
|
category: 'Apps',
|
||||||
|
method: 'GET',
|
||||||
|
path: '/api/v1/canvas-endpoints',
|
||||||
|
description: 'Integration with Canvas LMS',
|
||||||
|
publicPath: '/canvas-endpoints',
|
||||||
|
enterprises: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
label: 'FuseMind',
|
||||||
|
name: 'FuseMind',
|
||||||
|
businessLabel: 'FuseMind',
|
||||||
|
urlSlug: 'fusemind',
|
||||||
|
logo: fusero_logo,
|
||||||
|
category: 'Apps',
|
||||||
|
method: 'GET',
|
||||||
|
path: '/api/v1/fusemind',
|
||||||
|
description: 'FuseMind AI Platform',
|
||||||
|
publicPath: '/fusemind',
|
||||||
|
enterprises: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const SystemSelectionModal = ({ systemDetails }: SystemSelectionModalProps) => {
|
const SystemSelectionModal = ({ systemDetails }: SystemSelectionModalProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [selectedSystem, setSelectedSystem] = useState<SystemDropdownOption | null>(null);
|
const [selectedSystem, setSelectedSystem] = useState<SystemDropdownOption | null>(null);
|
||||||
const [selectedEnterprise, setSelectedEnterprise] = useState<number | null>(null);
|
const [selectedEnterprise, setSelectedEnterprise] = useState<number | null>(null);
|
||||||
|
const [showAddSystemDialog, setShowAddSystemDialog] = useState(false);
|
||||||
|
const [showEditLogoDialog, setShowEditLogoDialog] = useState(false);
|
||||||
|
const [systems, setSystems] = useState<SystemDropdownOption[]>(HARDCODED_SYSTEMS);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [uploadedLogo, setUploadedLogo] = useState<string | null>(null);
|
||||||
|
const [newSystem, setNewSystem] = useState({
|
||||||
|
name: '',
|
||||||
|
businessLabel: '',
|
||||||
|
urlSlug: '',
|
||||||
|
method: '',
|
||||||
|
path: '',
|
||||||
|
description: '',
|
||||||
|
publicPath: '',
|
||||||
|
});
|
||||||
const { setSystem } = useSystemStore();
|
const { setSystem } = useSystemStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -91,7 +117,7 @@ const SystemSelectionModal = ({ systemDetails }: SystemSelectionModalProps) => {
|
|||||||
|
|
||||||
const handleSelectSystem = () => {
|
const handleSelectSystem = () => {
|
||||||
if (selectedSystem) {
|
if (selectedSystem) {
|
||||||
navigate(`/dashboard/${selectedSystem.urlSlug}`); // JUST GO TO BASE PATH
|
navigate(`/dashboard/${selectedSystem.urlSlug}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -99,6 +125,144 @@ const SystemSelectionModal = ({ systemDetails }: SystemSelectionModalProps) => {
|
|||||||
setSelectedEnterprise(e.value);
|
setSelectedEnterprise(e.value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleLogoUpload = async (event: any) => {
|
||||||
|
const file = event.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const base64 = e.target?.result as string;
|
||||||
|
setUploadedLogo(base64);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateLogo = async () => {
|
||||||
|
if (!selectedSystem || !uploadedLogo) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Upload the new logo
|
||||||
|
const logoResponse = await fetch('/api/v1/assets', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: `${selectedSystem.name}-logo`,
|
||||||
|
type: 'image',
|
||||||
|
data: uploadedLogo,
|
||||||
|
mimeType: 'image/png',
|
||||||
|
assetType: 'system_logo',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!logoResponse.ok) {
|
||||||
|
throw new Error('Failed to upload logo');
|
||||||
|
}
|
||||||
|
|
||||||
|
const logoData = await logoResponse.json();
|
||||||
|
|
||||||
|
// Update the system with the new logo ID
|
||||||
|
const response = await fetch(`/api/v1/apps/${selectedSystem.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
logoId: logoData.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to update system logo');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSystem = await response.json();
|
||||||
|
|
||||||
|
// Update the local state
|
||||||
|
setSystems(prev => prev.map(system =>
|
||||||
|
system.id === selectedSystem.id
|
||||||
|
? { ...system, logo: updatedSystem.logo || fusero_logo }
|
||||||
|
: system
|
||||||
|
));
|
||||||
|
|
||||||
|
setSelectedSystem(prev => prev ? { ...prev, logo: updatedSystem.logo || fusero_logo } : null);
|
||||||
|
setShowEditLogoDialog(false);
|
||||||
|
setUploadedLogo(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating logo:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddSystem = async () => {
|
||||||
|
try {
|
||||||
|
// First upload the logo if exists
|
||||||
|
let logoId = null;
|
||||||
|
if (uploadedLogo) {
|
||||||
|
const logoResponse = await fetch('/api/v1/assets', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: `${newSystem.name}-logo`,
|
||||||
|
type: 'image',
|
||||||
|
data: uploadedLogo,
|
||||||
|
mimeType: 'image/png',
|
||||||
|
assetType: 'system_logo',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!logoResponse.ok) {
|
||||||
|
throw new Error('Failed to upload logo');
|
||||||
|
}
|
||||||
|
|
||||||
|
const logoData = await logoResponse.json();
|
||||||
|
logoId = logoData.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then create the system
|
||||||
|
const response = await fetch('/api/v1/apps', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...newSystem,
|
||||||
|
logoId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to create system');
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdSystem = await response.json();
|
||||||
|
const systemOption: SystemDropdownOption = {
|
||||||
|
...createdSystem,
|
||||||
|
label: createdSystem.name,
|
||||||
|
businessLabel: createdSystem.businessLabel || createdSystem.name,
|
||||||
|
logo: createdSystem.logo || fusero_logo,
|
||||||
|
category: 'Apps',
|
||||||
|
};
|
||||||
|
|
||||||
|
setSystems(prev => [...prev, systemOption]);
|
||||||
|
setShowAddSystemDialog(false);
|
||||||
|
setNewSystem({
|
||||||
|
name: '',
|
||||||
|
businessLabel: '',
|
||||||
|
urlSlug: '',
|
||||||
|
method: '',
|
||||||
|
path: '',
|
||||||
|
description: '',
|
||||||
|
publicPath: '',
|
||||||
|
});
|
||||||
|
setUploadedLogo(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating system:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card
|
<Card
|
||||||
@ -106,15 +270,22 @@ const SystemSelectionModal = ({ systemDetails }: SystemSelectionModalProps) => {
|
|||||||
className='px-8 bg-white'
|
className='px-8 bg-white'
|
||||||
>
|
>
|
||||||
<h2 className='my-2 text-xl font-semibold text-gray-700'>Apps & Flows</h2>
|
<h2 className='my-2 text-xl font-semibold text-gray-700'>Apps & Flows</h2>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center gap-4'>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
id='system'
|
id='system'
|
||||||
value={selectedSystem}
|
value={selectedSystem}
|
||||||
options={mockedSystems}
|
options={systems}
|
||||||
onChange={handleSystemChange}
|
onChange={handleSystemChange}
|
||||||
optionLabel='businessLabel'
|
optionLabel='businessLabel'
|
||||||
placeholder='Select an integration'
|
placeholder='Select an integration'
|
||||||
|
className='flex-1'
|
||||||
|
loading={isLoading}
|
||||||
/>
|
/>
|
||||||
|
{/* <Button
|
||||||
|
label='Add System'
|
||||||
|
onClick={() => setShowAddSystemDialog(true)}
|
||||||
|
className='px-4 py-2 text-white transition-colors bg-blue-500 border-2 border-blue-500 rounded-md hover:bg-white hover:text-blue-500'
|
||||||
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
{selectedSystem && selectedSystem.enterprises && (
|
{selectedSystem && selectedSystem.enterprises && (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@ -136,12 +307,170 @@ const SystemSelectionModal = ({ systemDetails }: SystemSelectionModalProps) => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
</Card>
|
</Card>
|
||||||
{selectedSystem && (
|
{selectedSystem && (
|
||||||
|
<div className="relative">
|
||||||
<img
|
<img
|
||||||
src={selectedSystem.logo}
|
src={selectedSystem.logo}
|
||||||
alt={`${selectedSystem.label} Logo`}
|
alt={`${selectedSystem.label} Logo`}
|
||||||
className='w-48'
|
className='w-48'
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
label="Edit Logo"
|
||||||
|
onClick={() => setShowEditLogoDialog(true)}
|
||||||
|
className="absolute bottom-0 right-0 px-2 py-1 text-sm text-white bg-blue-500 rounded hover:bg-blue-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
header="Add New System"
|
||||||
|
visible={showAddSystemDialog}
|
||||||
|
style={{ width: '50vw' }}
|
||||||
|
onHide={() => setShowAddSystemDialog(false)}
|
||||||
|
footer={
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
label="Cancel"
|
||||||
|
onClick={() => setShowAddSystemDialog(false)}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Create"
|
||||||
|
onClick={handleAddSystem}
|
||||||
|
className="bg-blue-500 text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2">Name</label>
|
||||||
|
<InputText
|
||||||
|
value={newSystem.name}
|
||||||
|
onChange={(e) => setNewSystem({ ...newSystem, name: e.target.value })}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2">Business Label</label>
|
||||||
|
<InputText
|
||||||
|
value={newSystem.businessLabel}
|
||||||
|
onChange={(e) => setNewSystem({ ...newSystem, businessLabel: e.target.value })}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2">URL Slug</label>
|
||||||
|
<InputText
|
||||||
|
value={newSystem.urlSlug}
|
||||||
|
onChange={(e) => setNewSystem({ ...newSystem, urlSlug: e.target.value })}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2">Method</label>
|
||||||
|
<InputText
|
||||||
|
value={newSystem.method}
|
||||||
|
onChange={(e) => setNewSystem({ ...newSystem, method: e.target.value })}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2">Path</label>
|
||||||
|
<InputText
|
||||||
|
value={newSystem.path}
|
||||||
|
onChange={(e) => setNewSystem({ ...newSystem, path: e.target.value })}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2">Description</label>
|
||||||
|
<InputTextarea
|
||||||
|
value={newSystem.description}
|
||||||
|
onChange={(e) => setNewSystem({ ...newSystem, description: e.target.value })}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2">Public Path</label>
|
||||||
|
<InputText
|
||||||
|
value={newSystem.publicPath}
|
||||||
|
onChange={(e) => setNewSystem({ ...newSystem, publicPath: e.target.value })}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2">Logo</label>
|
||||||
|
<FileUpload
|
||||||
|
mode="basic"
|
||||||
|
name="logo"
|
||||||
|
accept="image/*"
|
||||||
|
maxFileSize={1000000}
|
||||||
|
chooseLabel="Upload Logo"
|
||||||
|
onUpload={handleLogoUpload}
|
||||||
|
auto
|
||||||
|
customUpload
|
||||||
|
/>
|
||||||
|
{uploadedLogo && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<img src={uploadedLogo} alt="Uploaded logo" className="w-32 h-32 object-contain" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
header="Update System Logo"
|
||||||
|
visible={showEditLogoDialog}
|
||||||
|
style={{ width: '30vw' }}
|
||||||
|
onHide={() => setShowEditLogoDialog(false)}
|
||||||
|
footer={
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
label="Cancel"
|
||||||
|
onClick={() => setShowEditLogoDialog(false)}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Update"
|
||||||
|
onClick={handleUpdateLogo}
|
||||||
|
className="bg-blue-500 text-white"
|
||||||
|
disabled={!uploadedLogo}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2">Current Logo</label>
|
||||||
|
{selectedSystem && (
|
||||||
|
<img
|
||||||
|
src={selectedSystem.logo}
|
||||||
|
alt="Current logo"
|
||||||
|
className="w-32 h-32 object-contain mb-4"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block mb-2">New Logo</label>
|
||||||
|
<FileUpload
|
||||||
|
mode="basic"
|
||||||
|
name="logo"
|
||||||
|
accept="image/*"
|
||||||
|
maxFileSize={1000000}
|
||||||
|
chooseLabel="Upload New Logo"
|
||||||
|
onUpload={handleLogoUpload}
|
||||||
|
auto
|
||||||
|
customUpload
|
||||||
|
/>
|
||||||
|
{uploadedLogo && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<img src={uploadedLogo} alt="New logo preview" className="w-32 h-32 object-contain" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -14,6 +14,12 @@ import ChatGptModal from './ChatGPTModal';
|
|||||||
import { Toast } from 'primereact/toast';
|
import { Toast } from 'primereact/toast';
|
||||||
import { useAuthStore } from '../../state/stores/useAuthStore';
|
import { useAuthStore } from '../../state/stores/useAuthStore';
|
||||||
import SettingsMenu, { SettingsMenuItem } from '../../shared/components/SettingsMenu';
|
import SettingsMenu, { SettingsMenuItem } from '../../shared/components/SettingsMenu';
|
||||||
|
import ManageDocsModal from './ManageDocsModal';
|
||||||
|
import PromptCreatorModal from './PromptCreatorModal';
|
||||||
|
import { Tooltip } from 'primereact/tooltip';
|
||||||
|
|
||||||
|
const CANVAS_APP_ID = 1; // Hardcoded Canvas app ID
|
||||||
|
const CANVAS_API_BASE_URL = import.meta.env.VITE_CANVAS_API_BASE_URL || 'https://canvas.instructure.com/api/v1';
|
||||||
|
|
||||||
export default function CanvasEndpoints() {
|
export default function CanvasEndpoints() {
|
||||||
const [endpoints, setEndpoints] = useState<any[]>([]);
|
const [endpoints, setEndpoints] = useState<any[]>([]);
|
||||||
@ -47,7 +53,7 @@ export default function CanvasEndpoints() {
|
|||||||
const [apiKey, setApiKey] = useState<string | null>(null);
|
const [apiKey, setApiKey] = useState<string | null>(null);
|
||||||
const [apiKeyLoading, setApiKeyLoading] = useState(false);
|
const [apiKeyLoading, setApiKeyLoading] = useState(false);
|
||||||
const [apiKeyError, setApiKeyError] = useState<string | null>(null);
|
const [apiKeyError, setApiKeyError] = useState<string | null>(null);
|
||||||
const [appId, setAppId] = useState<number | null>(null);
|
const [appId, setAppId] = useState<number | null>(CANVAS_APP_ID);
|
||||||
const [showApiKeyModal, setShowApiKeyModal] = useState(false);
|
const [showApiKeyModal, setShowApiKeyModal] = useState(false);
|
||||||
const [editByVoiceEndpoint, setEditByVoiceEndpoint] = useState<any>(null);
|
const [editByVoiceEndpoint, setEditByVoiceEndpoint] = useState<any>(null);
|
||||||
const [showVoiceModal, setShowVoiceModal] = useState(false);
|
const [showVoiceModal, setShowVoiceModal] = useState(false);
|
||||||
@ -56,17 +62,13 @@ export default function CanvasEndpoints() {
|
|||||||
const [fullTranscript, setFullTranscript] = useState('');
|
const [fullTranscript, setFullTranscript] = useState('');
|
||||||
const [showCogMenu, setShowCogMenu] = useState(false);
|
const [showCogMenu, setShowCogMenu] = useState(false);
|
||||||
const cogMenuRef = useRef<any>(null);
|
const cogMenuRef = useRef<any>(null);
|
||||||
|
const [showManageDocs, setShowManageDocs] = useState(false);
|
||||||
|
const [showPromptCreator, setShowPromptCreator] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchAppId() {
|
// Load API key from localStorage if available
|
||||||
try {
|
const storedKey = localStorage.getItem('canvas_api_key');
|
||||||
const response = await api('get', '/api/v1/app/by-name/Canvas');
|
if (storedKey) setApiKey(storedKey);
|
||||||
setAppId(response.data.id);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch Canvas app id');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchAppId();
|
|
||||||
initFilters();
|
initFilters();
|
||||||
fetchEndpoints();
|
fetchEndpoints();
|
||||||
}, []);
|
}, []);
|
||||||
@ -272,6 +274,7 @@ export default function CanvasEndpoints() {
|
|||||||
if (!appId) throw new Error('App ID not loaded');
|
if (!appId) throw new Error('App ID not loaded');
|
||||||
const response = await api('post', '/api/v1/app/apikey/generate', { userId: user.id, appId });
|
const response = await api('post', '/api/v1/app/apikey/generate', { userId: user.id, appId });
|
||||||
setApiKey(response.data.apiKey);
|
setApiKey(response.data.apiKey);
|
||||||
|
localStorage.setItem('canvas_api_key', response.data.apiKey);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const errorMessage = error.response?.data?.error || error.message || 'Failed to fetch or generate API key';
|
const errorMessage = error.response?.data?.error || error.message || 'Failed to fetch or generate API key';
|
||||||
setApiKeyError(errorMessage);
|
setApiKeyError(errorMessage);
|
||||||
@ -287,6 +290,9 @@ export default function CanvasEndpoints() {
|
|||||||
if (!user || !appId) return;
|
if (!user || !appId) return;
|
||||||
const response = await api('post', '/api/v1/app/apikey/get', { userId: user.id, appId });
|
const response = await api('post', '/api/v1/app/apikey/get', { userId: user.id, appId });
|
||||||
setApiKey(response.data.apiKey || null);
|
setApiKey(response.data.apiKey || null);
|
||||||
|
if (response.data.apiKey) {
|
||||||
|
localStorage.setItem('canvas_api_key', response.data.apiKey);
|
||||||
|
}
|
||||||
setApiKeyError(null);
|
setApiKeyError(null);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setApiKey(null);
|
setApiKey(null);
|
||||||
@ -531,6 +537,29 @@ export default function CanvasEndpoints() {
|
|||||||
recognition.start();
|
recognition.start();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const settingsItems = [
|
||||||
|
{
|
||||||
|
label: 'Manage API Key',
|
||||||
|
icon: 'pi pi-key',
|
||||||
|
command: () => setShowApiKeyModal(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Voice Command List',
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
command: () => setShowVoiceInfoModal(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Manage API Docs',
|
||||||
|
icon: 'pi pi-link',
|
||||||
|
command: () => setShowManageDocs(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Create Prompt',
|
||||||
|
icon: 'pi pi-plus-circle',
|
||||||
|
command: () => setShowPromptCreator(true),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const renderHeader = () => (
|
const renderHeader = () => (
|
||||||
<TableGroupHeader
|
<TableGroupHeader
|
||||||
size={size}
|
size={size}
|
||||||
@ -553,18 +582,7 @@ export default function CanvasEndpoints() {
|
|||||||
className="p-button-primary"
|
className="p-button-primary"
|
||||||
/>
|
/>
|
||||||
<SettingsMenu
|
<SettingsMenu
|
||||||
items={[
|
items={settingsItems}
|
||||||
{
|
|
||||||
label: 'Manage API Key',
|
|
||||||
icon: 'pi pi-key',
|
|
||||||
command: () => setShowApiKeyModal(true),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Voice Command List',
|
|
||||||
icon: 'pi pi-info-circle',
|
|
||||||
command: () => setShowVoiceInfoModal(true),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
buttonClassName="p-button-secondary"
|
buttonClassName="p-button-secondary"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@ -578,11 +596,38 @@ export default function CanvasEndpoints() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const serverPath = import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
||||||
|
|
||||||
|
const generateCurlCommand = (endpoint: any, apiKey: string) => {
|
||||||
|
const backendUrl = `${serverPath}/api/v1/canvas-api/proxy-external`;
|
||||||
|
const method = endpoint.method || 'GET';
|
||||||
|
const path = endpoint.path;
|
||||||
|
const header = `-H 'Authorization: Bearer ${apiKey}'`;
|
||||||
|
const data = `-d '{"path": "${path}", "method": "${method}"}'`;
|
||||||
|
return `curl -X POST ${header} -H 'Content-Type: application/json' ${data} '${backendUrl}'`;
|
||||||
|
};
|
||||||
|
|
||||||
const renderCallButton = (rowData: any) => (
|
const renderCallButton = (rowData: any) => (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button icon="pi pi-play" onClick={() => handleCallEndpoint(rowData)} />
|
<Button icon="pi pi-play" onClick={() => handleCallEndpoint(rowData)} />
|
||||||
<Button icon="pi pi-pencil" className="p-button-warning" onClick={() => openEditModal(rowData)} />
|
<Button icon="pi pi-pencil" className="p-button-warning" onClick={() => openEditModal(rowData)} />
|
||||||
<Button icon="pi pi-trash" className="p-button-danger" onClick={() => handleDeleteEndpoint(rowData)} />
|
<Button icon="pi pi-trash" className="p-button-danger" onClick={() => handleDeleteEndpoint(rowData)} />
|
||||||
|
<span>
|
||||||
|
<Button icon="pi pi-copy" className="p-button-secondary" onClick={() => {
|
||||||
|
if (!apiKey) {
|
||||||
|
if (toast.current) {
|
||||||
|
toast.current.show({ severity: 'warn', summary: 'API Key Required', detail: 'Please generate or enter your API key first.' });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const curl = generateCurlCommand(rowData, apiKey);
|
||||||
|
navigator.clipboard.writeText(curl);
|
||||||
|
if (toast.current) {
|
||||||
|
toast.current.show({ severity: 'info', summary: 'Copied', detail: 'cURL command copied to clipboard!' });
|
||||||
|
}
|
||||||
|
}} data-pr-tooltip="Copy cURL command" />
|
||||||
|
<Tooltip target=".p-button-secondary" content="Copy cURL command" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -635,11 +680,13 @@ export default function CanvasEndpoints() {
|
|||||||
<Dialog
|
<Dialog
|
||||||
visible={showCreateModal}
|
visible={showCreateModal}
|
||||||
onHide={handleModalHide}
|
onHide={handleModalHide}
|
||||||
header={editEndpointId ? "Edit" : "Create"}
|
header={editEndpointId ? 'Edit Endpoint' : 'Create Endpoint'}
|
||||||
|
style={{ width: '40vw' }}
|
||||||
|
modal
|
||||||
footer={
|
footer={
|
||||||
<div>
|
<div>
|
||||||
<Button label="Cancel" icon="pi pi-times" onClick={handleModalHide} className="p-button-text" />
|
<Button label="Cancel" onClick={handleModalHide} className="mr-2" />
|
||||||
<Button label={editEndpointId ? "Save" : "Create"} icon="pi pi-check" onClick={handleSaveEndpoint} autoFocus disabled={!(newEndpoint.name && newEndpoint.method && newEndpoint.path)} />
|
<Button label={editEndpointId ? 'Save' : 'Create'} onClick={handleSaveEndpoint} className="bg-blue-500 text-white" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -650,18 +697,7 @@ export default function CanvasEndpoints() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-field">
|
<div className="p-field">
|
||||||
<label htmlFor="method">Method</label>
|
<label htmlFor="method">Method</label>
|
||||||
<Dropdown
|
<InputText id="method" value={newEndpoint.method} onChange={(e) => setNewEndpoint({ ...newEndpoint, method: e.target.value })} />
|
||||||
id="method"
|
|
||||||
value={newEndpoint.method}
|
|
||||||
options={[
|
|
||||||
{ label: 'GET', value: 'GET' },
|
|
||||||
{ label: 'POST', value: 'POST' },
|
|
||||||
{ label: 'PUT', value: 'PUT' },
|
|
||||||
{ label: 'DELETE', value: 'DELETE' }
|
|
||||||
]}
|
|
||||||
onChange={(e) => setNewEndpoint({ ...newEndpoint, method: e.value })}
|
|
||||||
placeholder="Select a method"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="p-field">
|
<div className="p-field">
|
||||||
<label htmlFor="path">Path</label>
|
<label htmlFor="path">Path</label>
|
||||||
@ -855,6 +891,14 @@ export default function CanvasEndpoints() {
|
|||||||
<li><b>open command list</b> — Show this help</li>
|
<li><b>open command list</b> — Show this help</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<ManageDocsModal visible={showManageDocs} onHide={() => setShowManageDocs(false)} />
|
||||||
|
|
||||||
|
<PromptCreatorModal
|
||||||
|
visible={showPromptCreator}
|
||||||
|
onHide={() => setShowPromptCreator(false)}
|
||||||
|
appId={appId || 0}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
143
frontend/src/components/canvas-api/ManageDocsModal.tsx
Normal file
143
frontend/src/components/canvas-api/ManageDocsModal.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Dialog } from 'primereact/dialog';
|
||||||
|
import { InputText } from 'primereact/inputtext';
|
||||||
|
import { Button } from 'primereact/button';
|
||||||
|
import { ListBox } from 'primereact/listbox';
|
||||||
|
|
||||||
|
interface ManageDocsModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onHide: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParseResult {
|
||||||
|
rawHtml: string;
|
||||||
|
aiOutput: any;
|
||||||
|
restructured: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOCAL_STORAGE_KEY = 'apiDocUrls';
|
||||||
|
|
||||||
|
export function ManageDocsModal({ visible, onHide }: ManageDocsModalProps) {
|
||||||
|
const [urls, setUrls] = useState<string[]>([]);
|
||||||
|
const [selectedUrl, setSelectedUrl] = useState<string | null>(null);
|
||||||
|
const [newUrl, setNewUrl] = useState('');
|
||||||
|
const [editIndex, setEditIndex] = useState<number | null>(null);
|
||||||
|
const [editValue, setEditValue] = useState('');
|
||||||
|
const [parsing, setParsing] = useState(false);
|
||||||
|
const [parseResult, setParseResult] = useState<ParseResult | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Load URLs from localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||||
|
if (saved) setUrls(JSON.parse(saved));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Save URLs to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(urls));
|
||||||
|
}, [urls]);
|
||||||
|
|
||||||
|
const addUrl = () => {
|
||||||
|
if (newUrl && !urls.includes(newUrl)) {
|
||||||
|
setUrls([...urls, newUrl]);
|
||||||
|
setNewUrl('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeUrl = (url: string) => {
|
||||||
|
setUrls(urls.filter(u => u !== url));
|
||||||
|
if (selectedUrl === url) setSelectedUrl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startEdit = (idx: number) => {
|
||||||
|
setEditIndex(idx);
|
||||||
|
setEditValue(urls[idx]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveEdit = () => {
|
||||||
|
if (editIndex !== null && editValue) {
|
||||||
|
const newUrls = [...urls];
|
||||||
|
newUrls[editIndex] = editValue;
|
||||||
|
setUrls(newUrls);
|
||||||
|
setEditIndex(null);
|
||||||
|
setEditValue('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleParse = async (url: string) => {
|
||||||
|
setParsing(true);
|
||||||
|
setError(null);
|
||||||
|
setParseResult(null);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/parse-docs', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ url }),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch or parse docs');
|
||||||
|
const data = await response.json();
|
||||||
|
setParseResult(data);
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message || 'Unknown error');
|
||||||
|
} finally {
|
||||||
|
setParsing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog visible={visible} onHide={onHide} header="Manage API Docs" style={{ width: '60vw' }}>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
{/* URL List and Controls */}
|
||||||
|
<div style={{ minWidth: 300 }}>
|
||||||
|
<h4>Documentation URLs</h4>
|
||||||
|
<ListBox
|
||||||
|
value={selectedUrl}
|
||||||
|
options={urls.map(u => ({ label: u, value: u }))}
|
||||||
|
onChange={e => setSelectedUrl(e.value)}
|
||||||
|
style={{ width: '100%', minHeight: 200 }}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
<InputText
|
||||||
|
value={newUrl}
|
||||||
|
onChange={e => setNewUrl(e.target.value)}
|
||||||
|
placeholder="Add new URL"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<Button label="Add" onClick={addUrl} disabled={!newUrl} />
|
||||||
|
</div>
|
||||||
|
{selectedUrl && (
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
<Button label="Parse" onClick={() => handleParse(selectedUrl)} loading={parsing} />
|
||||||
|
<Button label="Remove" className="p-button-danger" onClick={() => removeUrl(selectedUrl)} />
|
||||||
|
<Button label="Edit" onClick={() => startEdit(urls.indexOf(selectedUrl))} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{editIndex !== null && (
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
<InputText value={editValue} onChange={e => setEditValue(e.target.value)} className="w-full" />
|
||||||
|
<Button label="Save" onClick={saveEdit} />
|
||||||
|
<Button label="Cancel" className="p-button-secondary" onClick={() => setEditIndex(null)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Parse Results */}
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
{error && <div className="text-red-500 mb-2">{error}</div>}
|
||||||
|
{parseResult && (
|
||||||
|
<div>
|
||||||
|
<h4>Raw HTML (first 500 chars):</h4>
|
||||||
|
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{parseResult.rawHtml?.slice(0, 500)}...</pre>
|
||||||
|
<h4>AI Output:</h4>
|
||||||
|
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{JSON.stringify(parseResult.aiOutput, null, 2)}</pre>
|
||||||
|
<h4>Restructured Output:</h4>
|
||||||
|
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{JSON.stringify(parseResult.restructured, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageDocsModal;
|
153
frontend/src/components/canvas-api/PromptCreatorModal.tsx
Normal file
153
frontend/src/components/canvas-api/PromptCreatorModal.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Dialog } from 'primereact/dialog';
|
||||||
|
import { InputText } from 'primereact/inputtext';
|
||||||
|
import { Dropdown } from 'primereact/dropdown';
|
||||||
|
import { Button } from 'primereact/button';
|
||||||
|
import { Toast } from 'primereact/toast';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { api } from '../../services/api';
|
||||||
|
import { InputTextarea } from 'primereact/inputtextarea';
|
||||||
|
|
||||||
|
interface PromptCreatorModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onHide: () => void;
|
||||||
|
appId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promptTypes = [
|
||||||
|
{ label: 'API Documentation', value: 'api_documentation' },
|
||||||
|
{ label: 'Endpoint Analysis', value: 'endpoint_analysis' },
|
||||||
|
{ label: 'Schema Generation', value: 'schema_generation' },
|
||||||
|
{ label: 'Custom', value: 'custom' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function PromptCreatorModal({ visible, onHide, appId }: PromptCreatorModalProps) {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [content, setContent] = useState('');
|
||||||
|
const [type, setType] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const toast = useRef<Toast>(null);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!name || !content || !type) {
|
||||||
|
toast.current?.show({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error',
|
||||||
|
detail: 'Please fill in all required fields',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await api('post', '/api/v1/prompts', {
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
type,
|
||||||
|
appId,
|
||||||
|
metadata: {
|
||||||
|
source: 'canvas-api',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.current?.show({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Success',
|
||||||
|
detail: 'Prompt created successfully',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process the prompt immediately
|
||||||
|
await api('post', `/api/v1/prompts/${response.data.id}/process`);
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setName('');
|
||||||
|
setContent('');
|
||||||
|
setType(null);
|
||||||
|
onHide();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating prompt:', error);
|
||||||
|
toast.current?.show({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error',
|
||||||
|
detail: 'Failed to create prompt',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Toast ref={toast} />
|
||||||
|
<Dialog
|
||||||
|
visible={visible}
|
||||||
|
onHide={onHide}
|
||||||
|
header="Create New Prompt"
|
||||||
|
style={{ width: '50vw' }}
|
||||||
|
footer={
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
label="Cancel"
|
||||||
|
icon="pi pi-times"
|
||||||
|
onClick={onHide}
|
||||||
|
className="p-button-text"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Create"
|
||||||
|
icon="pi pi-check"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
loading={loading}
|
||||||
|
disabled={!name || !content || !type}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4 p-4">
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Prompt Name
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Enter a name for your prompt..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="type" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Prompt Type
|
||||||
|
</label>
|
||||||
|
<Dropdown
|
||||||
|
id="type"
|
||||||
|
value={type}
|
||||||
|
options={promptTypes}
|
||||||
|
onChange={(e) => setType(e.value)}
|
||||||
|
placeholder="Select a type"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label htmlFor="content" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Prompt Content
|
||||||
|
</label>
|
||||||
|
<InputTextarea
|
||||||
|
id="content"
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
className="w-full"
|
||||||
|
rows={8}
|
||||||
|
autoResize
|
||||||
|
placeholder="Enter your prompt..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
61
frontend/src/components/canvas-api/api.ts
Normal file
61
frontend/src/components/canvas-api/api.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
interface ApiConfig {
|
||||||
|
baseURL?: string;
|
||||||
|
timeout?: number;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean base URL once at top-level
|
||||||
|
const rawBaseUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||||
|
const cleanedBaseUrl = rawBaseUrl.replace(/\/$/, '');
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: ApiConfig = {
|
||||||
|
baseURL: cleanedBaseUrl,
|
||||||
|
timeout: 30000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const api = async <T = any>(
|
||||||
|
method: 'get' | 'post' | 'put' | 'delete' | 'patch',
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
params?: any,
|
||||||
|
responseType: 'json' | 'blob' | 'text' = 'json',
|
||||||
|
timeout?: number,
|
||||||
|
useAuth: boolean = true
|
||||||
|
): Promise<AxiosResponse<T>> => {
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
params,
|
||||||
|
responseType,
|
||||||
|
timeout: timeout || DEFAULT_CONFIG.timeout,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (useAuth) {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers = {
|
||||||
|
...config.headers,
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await axios(config);
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
@ -67,7 +67,7 @@ const Sidebar = ({ systemId, isCollapsed, setIsCollapsed }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex justify-center w-full p-0 m-0'>
|
<div className='flex justify-center w-full p-0 m-0'>
|
||||||
<Link to='/dashboard'>
|
<Link to='/'>
|
||||||
<Button
|
<Button
|
||||||
label={isCollapsed ? '' : 'Back'}
|
label={isCollapsed ? '' : 'Back'}
|
||||||
icon='pi pi-arrow-left'
|
icon='pi pi-arrow-left'
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import axios, { AxiosResponse, ResponseType } from 'axios';
|
import axios, { AxiosResponse, ResponseType } from 'axios';
|
||||||
import { useApiLogStore } from './apiLogStore';
|
import { useApiLogStore } from './apiLogStore';
|
||||||
|
|
||||||
// Server path
|
// Clean server path, removing trailing slash if present
|
||||||
const serverPath = import.meta.env.VITE_API_BASE_URL as string;
|
const rawBaseUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||||
|
const serverPath = rawBaseUrl.replace(/\/$/, '');
|
||||||
|
|
||||||
type THttpMethod = 'get' | 'put' | 'post' | 'delete';
|
type THttpMethod = 'get' | 'put' | 'post' | 'delete';
|
||||||
|
|
||||||
@ -27,10 +28,9 @@ export async function api<T = any>(
|
|||||||
const token = user?.token || '';
|
const token = user?.token || '';
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only add auth headers if not skipping auth
|
|
||||||
if (!skipAuth && token) {
|
if (!skipAuth && token) {
|
||||||
headers['Authorization'] = `Bearer ${token}`;
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
@ -39,7 +39,7 @@ export async function api<T = any>(
|
|||||||
baseURL: serverPath,
|
baseURL: serverPath,
|
||||||
timeout: timeoutInSeconds * 1000,
|
timeout: timeoutInSeconds * 1000,
|
||||||
headers,
|
headers,
|
||||||
withCredentials: !skipAuth, // Only use credentials if not skipping auth
|
withCredentials: !skipAuth,
|
||||||
responseType,
|
responseType,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ export async function api<T = any>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return apiResponse;
|
return apiResponse;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('API Error:', {
|
console.error('API Error:', {
|
||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
|
@ -65,11 +65,15 @@ class MemoryService {
|
|||||||
const event = this.activeLifeEvents.get(formationContext.temporalContext.lifeEvent);
|
const event = this.activeLifeEvents.get(formationContext.temporalContext.lifeEvent);
|
||||||
if (event) {
|
if (event) {
|
||||||
memory.temporal.lifeEvents.push(event);
|
memory.temporal.lifeEvents.push(event);
|
||||||
memory.temporal.temporalContext = formationContext.temporalContext;
|
memory.temporal.temporalContext = {
|
||||||
|
period: formationContext.temporalContext.period,
|
||||||
|
moodState: this.getMoodState(),
|
||||||
|
cognitiveBias: this.getActiveBiases()
|
||||||
|
};
|
||||||
|
|
||||||
// Apply emotional biases
|
// Apply emotional biases
|
||||||
memory.emotional.triggers.push(...formationContext.emotionalInfluence.biasEffects.map(bias => ({
|
memory.emotional.triggers.push(...formationContext.emotionalInfluence.biasEffects.map(bias => ({
|
||||||
type: 'contextual',
|
type: 'contextual' as const,
|
||||||
source: 'life_event',
|
source: 'life_event',
|
||||||
intensity: bias.strength,
|
intensity: bias.strength,
|
||||||
associatedEvent: formationContext.temporalContext.lifeEvent,
|
associatedEvent: formationContext.temporalContext.lifeEvent,
|
||||||
@ -122,9 +126,18 @@ class MemoryService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async retrieveMemoryFromStorage(query: MemoryRetrieval): Promise<MemoryUnit[]> {
|
||||||
|
// TODO: Implement actual storage retrieval
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
async consolidateMemories(consolidation: MemoryConsolidation): Promise<MemoryUnit> {
|
async consolidateMemories(consolidation: MemoryConsolidation): Promise<MemoryUnit> {
|
||||||
const memories = await Promise.all(
|
const memories = await Promise.all(
|
||||||
consolidation.sourceMemories.map(id => this.retrieveMemory({ query: id }))
|
consolidation.sourceMemories.map(id => this.retrieveMemory({
|
||||||
|
query: id,
|
||||||
|
context: {},
|
||||||
|
filters: {}
|
||||||
|
}))
|
||||||
).then(results => results.flat());
|
).then(results => results.flat());
|
||||||
|
|
||||||
// Calculate emotional influence
|
// Calculate emotional influence
|
||||||
@ -321,7 +334,11 @@ class MemoryService {
|
|||||||
llmContext: Partial<LLMContextMemory['content']>
|
llmContext: Partial<LLMContextMemory['content']>
|
||||||
): Promise<MemoryUnit> {
|
): Promise<MemoryUnit> {
|
||||||
const memories = await Promise.all(
|
const memories = await Promise.all(
|
||||||
sourceMemories.map(id => this.retrieveMemory({ query: id }))
|
sourceMemories.map(id => this.retrieveMemory({
|
||||||
|
query: id,
|
||||||
|
context: {},
|
||||||
|
filters: {}
|
||||||
|
}))
|
||||||
).then(results => results.flat());
|
).then(results => results.flat());
|
||||||
|
|
||||||
// Create new consolidated memory with LLM context
|
// Create new consolidated memory with LLM context
|
||||||
|
@ -102,6 +102,8 @@ export interface EpisodicMemory extends MemoryUnit {
|
|||||||
context: Record<string, any>;
|
context: Record<string, any>;
|
||||||
emotionalSignificance: number;
|
emotionalSignificance: number;
|
||||||
};
|
};
|
||||||
|
confidence: number;
|
||||||
|
source: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,6 +124,8 @@ export interface SemanticMemory extends MemoryUnit {
|
|||||||
category: string;
|
category: string;
|
||||||
reliability: number;
|
reliability: number;
|
||||||
};
|
};
|
||||||
|
confidence: number;
|
||||||
|
source: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,6 +142,8 @@ export interface ProceduralMemory extends MemoryUnit {
|
|||||||
proficiency: number;
|
proficiency: number;
|
||||||
lastPracticed: Date;
|
lastPracticed: Date;
|
||||||
};
|
};
|
||||||
|
confidence: number;
|
||||||
|
source: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,6 +231,8 @@ export interface LLMContextMemory extends MemoryUnit {
|
|||||||
tokensPerSecond: number;
|
tokensPerSecond: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
confidence: number;
|
||||||
|
source: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,8 +247,10 @@ export interface EmbeddingMemory extends MemoryUnit {
|
|||||||
};
|
};
|
||||||
metadata: {
|
metadata: {
|
||||||
similarityThreshold: number;
|
similarityThreshold: number;
|
||||||
retrievalMethod: 'cosine' | 'euclidean' | 'dot';
|
retrievalMethod: 'dot' | 'cosine' | 'euclidean';
|
||||||
};
|
};
|
||||||
|
confidence: number;
|
||||||
|
source: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,6 +271,8 @@ export interface PromptMemory extends MemoryUnit {
|
|||||||
successRate: number;
|
successRate: number;
|
||||||
averageTokens: number;
|
averageTokens: number;
|
||||||
};
|
};
|
||||||
|
confidence: number;
|
||||||
|
source: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,7 +319,7 @@ export interface LifeEventMemory extends MemoryUnit {
|
|||||||
content: {
|
content: {
|
||||||
data: {
|
data: {
|
||||||
event: {
|
event: {
|
||||||
type: 'breakup' | 'loss' | 'achievement' | 'trauma' | 'transition';
|
type: 'transition' | 'breakup' | 'loss' | 'achievement' | 'trauma';
|
||||||
description: string;
|
description: string;
|
||||||
participants: string[];
|
participants: string[];
|
||||||
location?: string;
|
location?: string;
|
||||||
@ -344,6 +356,8 @@ export interface LifeEventMemory extends MemoryUnit {
|
|||||||
healingProgress: number;
|
healingProgress: number;
|
||||||
relatedEvents: string[];
|
relatedEvents: string[];
|
||||||
};
|
};
|
||||||
|
confidence: number;
|
||||||
|
source: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
// import 'dotenv/config';
|
|
||||||
|
|
||||||
|
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
// Force reload `.env` even if it was previously loaded
|
// Force reload `.env` even if it was previously loaded
|
||||||
@ -27,12 +24,11 @@ const config: Options = {
|
|||||||
warnWhenNoEntities: true,
|
warnWhenNoEntities: true,
|
||||||
disableDynamicFileAccess: false,
|
disableDynamicFileAccess: false,
|
||||||
},
|
},
|
||||||
dbName: process.env.POSTGRES_NAME,
|
dbName: process.env.POSTGRES_NAME || 'fusero-boilerplate-db',
|
||||||
host: process.env.POSTGRES_HOSTNAME,
|
host: process.env.POSTGRES_HOSTNAME || 'localhost',
|
||||||
port: Number(process.env.POSTGRES_PORT),
|
port: Number(process.env.POSTGRES_PORT) || 5432,
|
||||||
// port: parseInt(process.env.POSTGRES_PORT || "5432", 10),
|
user: process.env.POSTGRES_USER || 'root',
|
||||||
user: process.env.POSTGRES_USER,
|
password: process.env.POSTGRES_PASSWORD || 'root123',
|
||||||
password: process.env.POSTGRES_PASSWORD,
|
|
||||||
debug: !isProduction,
|
debug: !isProduction,
|
||||||
migrations: {
|
migrations: {
|
||||||
tableName: process.env.POSTGRES_NAME,
|
tableName: process.env.POSTGRES_NAME,
|
||||||
|
27
nginx/README.md
Normal file
27
nginx/README.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Nginx SSL Reverse Proxy Setup
|
||||||
|
|
||||||
|
## Generate Self-Signed Certificates
|
||||||
|
|
||||||
|
Run this script from the `nginx/` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./generate-selfsigned.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create `certs/fusero-selfsigned.crt` and `certs/fusero-selfsigned.key`.
|
||||||
|
|
||||||
|
## Usage with Docker Compose
|
||||||
|
|
||||||
|
1. Make sure the certs are generated as above.
|
||||||
|
2. Start the stack from the project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
- Nginx will listen on ports 443 (HTTPS) and 80 (HTTP, redirected to HTTPS).
|
||||||
|
- All traffic will be routed securely to your frontend and backend containers.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Browsers will warn about the self-signed certificate. You can safely bypass this for development/testing.
|
||||||
|
- For production, use a trusted certificate authority.
|
8
nginx/generate-selfsigned.sh
Normal file
8
nginx/generate-selfsigned.sh
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
mkdir -p certs
|
||||||
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||||
|
-keyout certs/fusero-selfsigned.key \
|
||||||
|
-out certs/fusero-selfsigned.crt \
|
||||||
|
-subj "/CN=localhost"
|
||||||
|
echo "Self-signed certificate generated in nginx/certs/"
|
38
nginx/nginx.conf
Normal file
38
nginx/nginx.conf
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Nginx SSL reverse proxy config
|
||||||
|
|
||||||
|
# Redirect HTTP to HTTPS
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS server
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/certs/fusero-selfsigned.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/certs/fusero-selfsigned.key;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
|
||||||
|
# Proxy frontend
|
||||||
|
location / {
|
||||||
|
proxy_pass http://fusero-frontend:80;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy API (backend)
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://fusero-app-boilerplate:14000/api/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
19
nginx/nginx.conf.dev
Normal file
19
nginx/nginx.conf.dev
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location ^~ /api/ {
|
||||||
|
proxy_pass http://fusero-app-backend:14000/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
38
nginx/nginx.conf.prod
Normal file
38
nginx/nginx.conf.prod
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Nginx SSL reverse proxy config
|
||||||
|
|
||||||
|
# Redirect HTTP to HTTPS
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS server
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/certs/fusero-selfsigned.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/certs/fusero-selfsigned.key;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
|
||||||
|
# Proxy frontend
|
||||||
|
location / {
|
||||||
|
proxy_pass http://fusero-frontend:80;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy API (backend)
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://fusero-app-boilerplate:14000/api/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
124
src/apps/_app/controllers/PromptController.ts
Normal file
124
src/apps/_app/controllers/PromptController.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { EntityManager } from '@mikro-orm/core';
|
||||||
|
import { PromptService } from '../services/PromptService';
|
||||||
|
import { PromptType } from '../entities/Prompt';
|
||||||
|
import { App } from '../entities/app/_App';
|
||||||
|
import { User } from '../entities/user/_User';
|
||||||
|
|
||||||
|
interface CreatePromptBody {
|
||||||
|
content: string;
|
||||||
|
type: PromptType;
|
||||||
|
appId: number;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PromptParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppParams {
|
||||||
|
appId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PromptController {
|
||||||
|
private promptService: PromptService;
|
||||||
|
|
||||||
|
constructor(private readonly em: EntityManager) {
|
||||||
|
this.promptService = new PromptService(em);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPrompt(
|
||||||
|
request: FastifyRequest<{ Body: CreatePromptBody }>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { content, type, appId, metadata } = request.body;
|
||||||
|
const user = request.user as User;
|
||||||
|
const app = await this.em.findOne(App, { id: appId });
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
return reply.status(404).send({ error: 'App not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = await this.promptService.createPrompt(
|
||||||
|
content,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
app,
|
||||||
|
metadata
|
||||||
|
);
|
||||||
|
|
||||||
|
return reply.status(201).send(prompt);
|
||||||
|
} catch (error) {
|
||||||
|
return reply.status(500).send({ error: 'Failed to create prompt' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processPrompt(
|
||||||
|
request: FastifyRequest<{ Params: PromptParams }>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const id = parseInt(request.params.id, 10);
|
||||||
|
|
||||||
|
if (isNaN(id)) {
|
||||||
|
return reply.status(400).send({ error: 'Invalid prompt ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = await this.promptService.processPrompt(id);
|
||||||
|
return reply.send(prompt);
|
||||||
|
} catch (error) {
|
||||||
|
return reply.status(500).send({ error: 'Failed to process prompt' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPrompt(
|
||||||
|
request: FastifyRequest<{ Params: PromptParams }>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const id = parseInt(request.params.id, 10);
|
||||||
|
|
||||||
|
if (isNaN(id)) {
|
||||||
|
return reply.status(400).send({ error: 'Invalid prompt ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = await this.promptService.getPromptById(id);
|
||||||
|
if (!prompt) {
|
||||||
|
return reply.status(404).send({ error: 'Prompt not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.send(prompt);
|
||||||
|
} catch (error) {
|
||||||
|
return reply.status(500).send({ error: 'Failed to get prompt' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserPrompts(request: FastifyRequest, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const user = request.user as User;
|
||||||
|
const prompts = await this.promptService.getPromptsByUser(user.id);
|
||||||
|
return reply.send(prompts);
|
||||||
|
} catch (error) {
|
||||||
|
return reply.status(500).send({ error: 'Failed to get user prompts' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAppPrompts(
|
||||||
|
request: FastifyRequest<{ Params: AppParams }>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const appId = parseInt(request.params.appId, 10);
|
||||||
|
|
||||||
|
if (isNaN(appId)) {
|
||||||
|
return reply.status(400).send({ error: 'Invalid app ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompts = await this.promptService.getPromptsByApp(appId);
|
||||||
|
return reply.send(prompts);
|
||||||
|
} catch (error) {
|
||||||
|
return reply.status(500).send({ error: 'Failed to get app prompts' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
src/apps/_app/entities/Prompt.ts
Normal file
48
src/apps/_app/entities/Prompt.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Entity, Property, ManyToOne, Enum } from '@mikro-orm/core';
|
||||||
|
import { BaseEntity } from './_BaseEntity';
|
||||||
|
import { User } from './user/_User';
|
||||||
|
import { App } from './app/_App';
|
||||||
|
|
||||||
|
export enum PromptType {
|
||||||
|
API_DOCUMENTATION = 'api_documentation',
|
||||||
|
ENDPOINT_ANALYSIS = 'endpoint_analysis',
|
||||||
|
SCHEMA_GENERATION = 'schema_generation',
|
||||||
|
CUSTOM = 'custom'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PromptStatus {
|
||||||
|
PENDING = 'pending',
|
||||||
|
PROCESSING = 'processing',
|
||||||
|
COMPLETED = 'completed',
|
||||||
|
FAILED = 'failed'
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Prompt extends BaseEntity {
|
||||||
|
@Property({ type: 'text', nullable: true })
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@Property({ type: 'text' })
|
||||||
|
content!: string;
|
||||||
|
|
||||||
|
@Property({ type: 'jsonb', default: '{}' })
|
||||||
|
metadata!: Record<string, any>;
|
||||||
|
|
||||||
|
@Enum(() => PromptType)
|
||||||
|
type!: PromptType;
|
||||||
|
|
||||||
|
@Enum(() => PromptStatus)
|
||||||
|
status: PromptStatus = PromptStatus.PENDING;
|
||||||
|
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
user!: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => App)
|
||||||
|
app!: App;
|
||||||
|
|
||||||
|
@Property({ type: 'jsonb', nullable: true })
|
||||||
|
processedResult?: Record<string, any>;
|
||||||
|
|
||||||
|
@Property({ type: 'text', nullable: true })
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
@ -7,6 +7,18 @@ export class App extends BaseEntity {
|
|||||||
@Property()
|
@Property()
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
method?: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
path?: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
publicPath?: string;
|
||||||
|
|
||||||
@OneToMany(() => TenantApp, (tenantApp) => tenantApp.app)
|
@OneToMany(() => TenantApp, (tenantApp) => tenantApp.app)
|
||||||
tenantApps = new Collection<TenantApp>(this);
|
tenantApps = new Collection<TenantApp>(this);
|
||||||
}
|
}
|
||||||
|
30
src/apps/_app/entities/asset/_Asset.ts
Normal file
30
src/apps/_app/entities/asset/_Asset.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Entity, Property, Enum } from '@mikro-orm/core';
|
||||||
|
import { BaseEntity } from '../_BaseEntity';
|
||||||
|
|
||||||
|
export enum AssetType {
|
||||||
|
SYSTEM_LOGO = 'system_logo',
|
||||||
|
USER_AVATAR = 'user_avatar',
|
||||||
|
DOCUMENT = 'document',
|
||||||
|
OTHER = 'other'
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Asset extends BaseEntity {
|
||||||
|
@Property()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Property()
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@Property({ type: 'text' })
|
||||||
|
data!: string; // Base64 encoded data
|
||||||
|
|
||||||
|
@Property()
|
||||||
|
mimeType!: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
size?: number;
|
||||||
|
|
||||||
|
@Enum(() => AssetType)
|
||||||
|
assetType!: AssetType;
|
||||||
|
}
|
30
src/apps/_app/middleware/auth.middleware.ts
Normal file
30
src/apps/_app/middleware/auth.middleware.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { EntityManager } from '@mikro-orm/core';
|
||||||
|
import { User } from '../entities/user/_User';
|
||||||
|
import '@fastify/jwt';
|
||||||
|
|
||||||
|
declare module '@fastify/jwt' {
|
||||||
|
interface FastifyJWT {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authMiddleware = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
try {
|
||||||
|
const em = request.server.orm.em as EntityManager;
|
||||||
|
const userId = request.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return reply.status(401).send({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await em.findOne(User, { id: userId });
|
||||||
|
if (!user) {
|
||||||
|
return reply.status(401).send({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
request.user = user;
|
||||||
|
} catch (error) {
|
||||||
|
return reply.status(500).send({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
};
|
115
src/apps/_app/routes/prompt.routes.ts
Normal file
115
src/apps/_app/routes/prompt.routes.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { PromptController } from '../controllers/PromptController';
|
||||||
|
import { authMiddleware } from '../middleware/auth.middleware';
|
||||||
|
import { PromptType } from '../entities/Prompt';
|
||||||
|
|
||||||
|
interface CreatePromptBody {
|
||||||
|
content: string;
|
||||||
|
type: PromptType;
|
||||||
|
appId: number;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PromptParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppParams {
|
||||||
|
appId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPromptSchema = {
|
||||||
|
body: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['content', 'type', 'appId'],
|
||||||
|
properties: {
|
||||||
|
content: { type: 'string' },
|
||||||
|
type: { type: 'string', enum: Object.values(PromptType) },
|
||||||
|
appId: { type: 'number' },
|
||||||
|
metadata: { type: 'object' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const promptParamsSchema = {
|
||||||
|
params: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['id'],
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const appParamsSchema = {
|
||||||
|
params: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['appId'],
|
||||||
|
properties: {
|
||||||
|
appId: { type: 'string' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function promptRoutes(fastify: FastifyInstance) {
|
||||||
|
const promptController = new PromptController(fastify.orm.em);
|
||||||
|
|
||||||
|
// Create a new prompt
|
||||||
|
fastify.post<{ Body: CreatePromptBody }>(
|
||||||
|
'/prompts',
|
||||||
|
{
|
||||||
|
schema: createPromptSchema,
|
||||||
|
preHandler: authMiddleware
|
||||||
|
},
|
||||||
|
async (request, reply) => {
|
||||||
|
return promptController.createPrompt(request, reply);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Process a prompt
|
||||||
|
fastify.post<{ Params: PromptParams }>(
|
||||||
|
'/prompts/:id/process',
|
||||||
|
{
|
||||||
|
schema: promptParamsSchema,
|
||||||
|
preHandler: authMiddleware
|
||||||
|
},
|
||||||
|
async (request, reply) => {
|
||||||
|
return promptController.processPrompt(request, reply);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get a prompt by ID
|
||||||
|
fastify.get<{ Params: PromptParams }>(
|
||||||
|
'/prompts/:id',
|
||||||
|
{
|
||||||
|
schema: promptParamsSchema,
|
||||||
|
preHandler: authMiddleware
|
||||||
|
},
|
||||||
|
async (request, reply) => {
|
||||||
|
return promptController.getPrompt(request, reply);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get all prompts for the current user
|
||||||
|
fastify.get(
|
||||||
|
'/prompts',
|
||||||
|
{
|
||||||
|
preHandler: authMiddleware
|
||||||
|
},
|
||||||
|
async (request, reply) => {
|
||||||
|
return promptController.getUserPrompts(request, reply);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get all prompts for a specific app
|
||||||
|
fastify.get<{ Params: AppParams }>(
|
||||||
|
'/apps/:appId/prompts',
|
||||||
|
{
|
||||||
|
schema: appParamsSchema,
|
||||||
|
preHandler: authMiddleware
|
||||||
|
},
|
||||||
|
async (request, reply) => {
|
||||||
|
return promptController.getAppPrompts(request, reply);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
122
src/apps/_app/services/PromptService.ts
Normal file
122
src/apps/_app/services/PromptService.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { EntityManager } from '@mikro-orm/core';
|
||||||
|
import { Prompt, PromptType, PromptStatus } from '../entities/Prompt';
|
||||||
|
import { User } from '../entities/user/_User';
|
||||||
|
import { App } from '../entities/app/_App';
|
||||||
|
|
||||||
|
interface PromptMetadata {
|
||||||
|
endpoints?: Array<{
|
||||||
|
path: string;
|
||||||
|
method: string;
|
||||||
|
statusCodes: number[];
|
||||||
|
}>;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProcessedResult {
|
||||||
|
endpoints?: Array<{
|
||||||
|
path: string;
|
||||||
|
method: string;
|
||||||
|
statusCodes: number[];
|
||||||
|
description?: string;
|
||||||
|
}>;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PromptService {
|
||||||
|
constructor(private readonly em: EntityManager) {}
|
||||||
|
|
||||||
|
async createPrompt(
|
||||||
|
content: string,
|
||||||
|
type: PromptType,
|
||||||
|
user: User,
|
||||||
|
app: App,
|
||||||
|
metadata: PromptMetadata = {}
|
||||||
|
): Promise<Prompt> {
|
||||||
|
const prompt = new Prompt();
|
||||||
|
prompt.content = content;
|
||||||
|
prompt.type = type;
|
||||||
|
prompt.user = user;
|
||||||
|
prompt.app = app;
|
||||||
|
prompt.metadata = metadata;
|
||||||
|
prompt.status = PromptStatus.PENDING;
|
||||||
|
|
||||||
|
await this.em.persistAndFlush(prompt);
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
async processPrompt(id: number): Promise<Prompt> {
|
||||||
|
const prompt = await this.getPromptById(id);
|
||||||
|
if (!prompt) {
|
||||||
|
throw new Error('Prompt not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
prompt.status = PromptStatus.PROCESSING;
|
||||||
|
await this.em.flush();
|
||||||
|
|
||||||
|
const result = await this.processPromptByType(prompt);
|
||||||
|
|
||||||
|
prompt.processedResult = result;
|
||||||
|
prompt.status = PromptStatus.COMPLETED;
|
||||||
|
await this.em.flush();
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
} catch (error) {
|
||||||
|
prompt.status = PromptStatus.FAILED;
|
||||||
|
prompt.errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
await this.em.flush();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processPromptByType(prompt: Prompt): Promise<ProcessedResult> {
|
||||||
|
switch (prompt.type) {
|
||||||
|
case PromptType.API_DOCUMENTATION:
|
||||||
|
return this.processApiDocumentation(prompt);
|
||||||
|
case PromptType.ENDPOINT_ANALYSIS:
|
||||||
|
return this.processEndpointAnalysis(prompt);
|
||||||
|
case PromptType.SCHEMA_GENERATION:
|
||||||
|
return this.processSchemaGeneration(prompt);
|
||||||
|
case PromptType.CUSTOM:
|
||||||
|
return this.processCustomPrompt(prompt);
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid prompt type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processApiDocumentation(prompt: Prompt): Promise<ProcessedResult> {
|
||||||
|
// TODO: Implement API documentation processing
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processEndpointAnalysis(prompt: Prompt): Promise<ProcessedResult> {
|
||||||
|
// TODO: Implement endpoint analysis processing
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processSchemaGeneration(prompt: Prompt): Promise<ProcessedResult> {
|
||||||
|
// TODO: Implement schema generation processing
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processCustomPrompt(prompt: Prompt): Promise<ProcessedResult> {
|
||||||
|
// TODO: Implement custom prompt processing
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPromptById(id: number): Promise<Prompt | null> {
|
||||||
|
return this.em.findOne(Prompt, { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPromptsByUser(userId: number): Promise<Prompt[]> {
|
||||||
|
return this.em.find(Prompt, { user: { id: userId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPromptsByApp(appId: number): Promise<Prompt[]> {
|
||||||
|
return this.em.find(Prompt, { app: { id: appId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPromptsByStatus(status: PromptStatus): Promise<Prompt[]> {
|
||||||
|
return this.em.find(Prompt, { status });
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
|
|||||||
import { CanvasApiEndpointController } from '../http/controllers/CanvasApiEndpointController';
|
import { CanvasApiEndpointController } from '../http/controllers/CanvasApiEndpointController';
|
||||||
import axios, { Method } from 'axios';
|
import axios, { Method } from 'axios';
|
||||||
|
|
||||||
const ALLOWED_METHODS: Method[] = ['GET']; // Expand as needed
|
const ALLOWED_METHODS: Method[] = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'];
|
||||||
|
|
||||||
// Type for proxy request input
|
// Type for proxy request input
|
||||||
interface CanvasProxyRequestInput {
|
interface CanvasProxyRequestInput {
|
||||||
@ -51,6 +51,7 @@ const endpointsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
let baseUrl = process.env.CANVAS_API_URL || 'https://talnet.instructure.com';
|
let baseUrl = process.env.CANVAS_API_URL || 'https://talnet.instructure.com';
|
||||||
// Remove trailing /api/v1 if present
|
// Remove trailing /api/v1 if present
|
||||||
baseUrl = baseUrl.replace(/\/api\/v1$/, '');
|
baseUrl = baseUrl.replace(/\/api\/v1$/, '');
|
||||||
|
// Always use the server's Canvas API key for Canvas requests
|
||||||
const apiKey = process.env.CANVAS_API_KEY || '';
|
const apiKey = process.env.CANVAS_API_KEY || '';
|
||||||
const { path, method = 'GET', params } = request.body;
|
const { path, method = 'GET', params } = request.body;
|
||||||
if (!path) return reply.status(400).send({ error: 'Missing path' });
|
if (!path) return reply.status(400).send({ error: 'Missing path' });
|
||||||
@ -58,6 +59,7 @@ const endpointsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
return reply.status(405).send({ error: `Method ${method} not allowed` });
|
return reply.status(405).send({ error: `Method ${method} not allowed` });
|
||||||
}
|
}
|
||||||
const url = `${baseUrl}/api/v1${path}`;
|
const url = `${baseUrl}/api/v1${path}`;
|
||||||
|
console.log('Proxying to Canvas:', { url, method, apiKey, params });
|
||||||
const response = await axios.request({
|
const response = await axios.request({
|
||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
@ -65,9 +67,11 @@ const endpointsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
params: method === 'GET' ? params : undefined,
|
params: method === 'GET' ? params : undefined,
|
||||||
data: method !== 'GET' ? params : undefined,
|
data: method !== 'GET' ? params : undefined,
|
||||||
});
|
});
|
||||||
|
console.log('Canvas response:', response.data);
|
||||||
return reply.send(response.data);
|
return reply.send(response.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return reply.status(500).send({ error: err.message });
|
console.error('Proxy error:', err);
|
||||||
|
return reply.status(500).send({ error: err.message, details: err.response?.data || err });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
50
src/database/migrations/Migration20240416000001.ts
Normal file
50
src/database/migrations/Migration20240416000001.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Migration } from '@mikro-orm/migrations';
|
||||||
|
|
||||||
|
export class Migration20240416000001 extends Migration {
|
||||||
|
async up(): Promise<void> {
|
||||||
|
this.addSql(`
|
||||||
|
create table "prompt" (
|
||||||
|
"id" serial primary key,
|
||||||
|
"created_at" timestamptz not null,
|
||||||
|
"updated_at" timestamptz not null,
|
||||||
|
"name" text null,
|
||||||
|
"content" text not null,
|
||||||
|
"metadata" jsonb not null default '{}',
|
||||||
|
"type" varchar(50) not null,
|
||||||
|
"status" varchar(50) not null default 'pending',
|
||||||
|
"user_id" int not null,
|
||||||
|
"app_id" int not null,
|
||||||
|
"processed_result" jsonb null,
|
||||||
|
"error_message" text null
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
this.addSql(`
|
||||||
|
alter table "prompt"
|
||||||
|
add constraint "prompt_user_id_foreign"
|
||||||
|
foreign key ("user_id")
|
||||||
|
references "user" ("id")
|
||||||
|
on update cascade
|
||||||
|
on delete cascade;
|
||||||
|
`);
|
||||||
|
|
||||||
|
this.addSql(`
|
||||||
|
alter table "prompt"
|
||||||
|
add constraint "prompt_app_id_foreign"
|
||||||
|
foreign key ("app_id")
|
||||||
|
references "app" ("id")
|
||||||
|
on update cascade
|
||||||
|
on delete cascade;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Add indexes for common queries
|
||||||
|
this.addSql('create index "prompt_user_id_index" on "prompt" ("user_id");');
|
||||||
|
this.addSql('create index "prompt_app_id_index" on "prompt" ("app_id");');
|
||||||
|
this.addSql('create index "prompt_status_index" on "prompt" ("status");');
|
||||||
|
this.addSql('create index "prompt_type_index" on "prompt" ("type");');
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(): Promise<void> {
|
||||||
|
this.addSql('drop table if exists "prompt" cascade;');
|
||||||
|
}
|
||||||
|
}
|
@ -3,15 +3,19 @@ import { Migration } from '@mikro-orm/migrations';
|
|||||||
export class Migration20250502161232 extends Migration {
|
export class Migration20250502161232 extends Migration {
|
||||||
|
|
||||||
override async up(): Promise<void> {
|
override async up(): Promise<void> {
|
||||||
this.addSql(`create table "canvas_api_endpoints" ("id" serial primary key, "created_at" timestamptz not null, "updated_at" timestamptz not null, "name" varchar(255) not null, "method" varchar(255) not null, "path" varchar(255) not null, "description" varchar(255) null, "user_id" int null);`);
|
// Add API-related columns to app table
|
||||||
|
this.addSql('alter table "app" add column "method" varchar(255) null;');
|
||||||
this.addSql(`alter table "canvas_api_endpoints" add constraint "canvas_api_endpoints_user_id_foreign" foreign key ("user_id") references "user" ("id") on update cascade on delete set null;`);
|
this.addSql('alter table "app" add column "path" varchar(255) null;');
|
||||||
|
this.addSql('alter table "app" add column "description" varchar(255) null;');
|
||||||
this.addSql('alter table "canvas_api_endpoints" add column "public_path" varchar(255) null;');
|
this.addSql('alter table "app" add column "public_path" varchar(255) null;');
|
||||||
}
|
}
|
||||||
|
|
||||||
override async down(): Promise<void> {
|
override async down(): Promise<void> {
|
||||||
this.addSql(`drop table if exists "canvas_api_endpoints" cascade;`);
|
// Remove the added columns
|
||||||
|
this.addSql('alter table "app" drop column "method";');
|
||||||
|
this.addSql('alter table "app" drop column "path";');
|
||||||
|
this.addSql('alter table "app" drop column "description";');
|
||||||
|
this.addSql('alter table "app" drop column "public_path";');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
23
src/database/migrations/Migration20250502161233.ts
Normal file
23
src/database/migrations/Migration20250502161233.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Migration } from '@mikro-orm/migrations';
|
||||||
|
|
||||||
|
export class Migration20250502161233 extends Migration {
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(`
|
||||||
|
create table "asset" (
|
||||||
|
"id" serial primary key,
|
||||||
|
"created_at" timestamptz not null,
|
||||||
|
"updated_at" timestamptz not null,
|
||||||
|
"name" varchar(255) not null,
|
||||||
|
"type" varchar(255) not null,
|
||||||
|
"data" text not null,
|
||||||
|
"mime_type" varchar(255) not null,
|
||||||
|
"size" int null,
|
||||||
|
"asset_type" varchar(255) not null
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql('drop table if exists "asset" cascade;');
|
||||||
|
}
|
||||||
|
}
|
20
src/database/seeds/CanvasAppSeed.ts
Normal file
20
src/database/seeds/CanvasAppSeed.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { EntityManager } from '@mikro-orm/core';
|
||||||
|
import { App } from '@/apps/_app/entities/app/_App';
|
||||||
|
|
||||||
|
export class CanvasAppSeed {
|
||||||
|
constructor(private readonly em: EntityManager) {}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
console.log('Seeding Canvas app/system...');
|
||||||
|
|
||||||
|
const existing = await this.em.findOne(App, { name: 'Canvas API' });
|
||||||
|
if (!existing) {
|
||||||
|
const app = new App();
|
||||||
|
app.name = 'Canvas API';
|
||||||
|
await this.em.persistAndFlush(app);
|
||||||
|
console.log('Created Canvas app/system.');
|
||||||
|
} else {
|
||||||
|
console.log('Canvas app/system already exists, skipping...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { EntityManager } from '@mikro-orm/core';
|
import { EntityManager } from '@mikro-orm/core';
|
||||||
import { UserSeed } from './UserSeed';
|
import { UserSeed } from './UserSeed';
|
||||||
import { RoleSeed } from './RoleSeed';
|
import { RoleSeed } from './RoleSeed';
|
||||||
|
import { CanvasAppSeed } from './CanvasAppSeed';
|
||||||
|
|
||||||
export class DatabaseSeeder {
|
export class DatabaseSeeder {
|
||||||
constructor(private readonly em: EntityManager) {}
|
constructor(private readonly em: EntityManager) {}
|
||||||
@ -19,6 +20,10 @@ export class DatabaseSeeder {
|
|||||||
const userSeeder = new UserSeed(fork);
|
const userSeeder = new UserSeed(fork);
|
||||||
await userSeeder.run();
|
await userSeeder.run();
|
||||||
|
|
||||||
|
// Then seed Canvas app/system
|
||||||
|
const canvasAppSeeder = new CanvasAppSeed(fork);
|
||||||
|
await canvasAppSeeder.run();
|
||||||
|
|
||||||
await fork.flush();
|
await fork.flush();
|
||||||
console.log('Database seeding completed!');
|
console.log('Database seeding completed!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { UserRoleType } from '@/constants/roles';
|
import { UserRoleType } from '../constants/roles';
|
||||||
|
import { User } from '../apps/_app/entities/user/_User';
|
||||||
|
import { EntityManager } from '@mikro-orm/core';
|
||||||
|
|
||||||
interface UserPayload extends jwt.JwtPayload {
|
interface UserPayload extends jwt.JwtPayload {
|
||||||
|
id: number;
|
||||||
roles: UserRoleType;
|
roles: UserRoleType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,7 +25,15 @@ const auth = (requiredRoles: UserRoleType[]) => async (request: FastifyRequest,
|
|||||||
return reply.code(403).send({ error: 'Unauthorized: Insufficient role' });
|
return reply.code(403).send({ error: 'Unauthorized: Insufficient role' });
|
||||||
}
|
}
|
||||||
|
|
||||||
request.user = decoded;
|
// Fetch the full user object from the database
|
||||||
|
const em = request.server.orm.em as EntityManager;
|
||||||
|
const user = await em.findOne(User, { id: decoded.id });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return reply.code(401).send({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
request.user = user;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const typedError = error as Error;
|
const typedError = error as Error;
|
||||||
return reply.code(401).send({ error: 'Authentication failed', info: typedError.message });
|
return reply.code(401).send({ error: 'Authentication failed', info: typedError.message });
|
||||||
|
Loading…
Reference in New Issue
Block a user