updates across the board

This commit is contained in:
liquidrinu 2025-05-15 15:10:07 +02:00
parent fc403142c2
commit 1152abd4a5
38 changed files with 1770 additions and 198 deletions

@ -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

@ -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;
}
}

@ -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>
); );
} }

@ -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;

@ -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>
</>
);
}

@ -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

@ -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.

@ -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

@ -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

@ -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

@ -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;
}
}

@ -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' });
}
}
}

@ -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);
} }

@ -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;
}

@ -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' });
}
};

@ -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);
}
);
}

@ -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 });
} }
}); });
}; };

@ -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";');
} }
} }

@ -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;');
}
}

@ -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 });