diff --git a/backend/src/controllers/DeviceController.ts b/backend/src/controllers/DeviceController.ts index e572438..fb57a9a 100644 --- a/backend/src/controllers/DeviceController.ts +++ b/backend/src/controllers/DeviceController.ts @@ -148,9 +148,13 @@ export class DeviceController { bookedDevice: device }); } + res.status(200).json(device); } catch (error) { - if (error instanceof Error && error.message === "ALREADY_BOOKED") { + if (error instanceof Error && error.message === "DEVICE_NOT_FOUND") { + // bypass as a success becuase it should just delete the topology + res.status(200); + } else if (error instanceof Error && error.message === "ALREADY_BOOKED") { res.status(409).json({ error: "Device already booked" }); } else { next(error); @@ -172,7 +176,10 @@ export class DeviceController { } res.status(200).json(device); } catch (error) { - if (error instanceof Error && error.message === "UNAUTHORIZED") { + if (error instanceof Error && error.message === "DEVICE_NOT_FOUND") { + // bypass as a success becuase it should just delete the topology + res.status(200); + } else if (error instanceof Error && error.message === "UNAUTHORIZED") { res.status(401).json({ error: "You are not authorized to unbook this device." }); } else { next(error); diff --git a/backend/src/repositories/PrismaDeviceRepository.ts b/backend/src/repositories/PrismaDeviceRepository.ts index d96e198..934e7d1 100644 --- a/backend/src/repositories/PrismaDeviceRepository.ts +++ b/backend/src/repositories/PrismaDeviceRepository.ts @@ -93,7 +93,10 @@ export class PrismaDeviceRepository implements IDeviceRepository { where: { id: deviceId }, }); - if (current?.userId && current.userId !== userId) { + if (!current) { + // device not found + throw new Error("DEVICE_NOT_FOUND"); + } else if (current?.userId && current.userId !== userId) { throw new Error("ALREADY_BOOKED"); } else if (current?.userId === userId) { // device is already booked by the same user, return current device @@ -116,6 +119,11 @@ export class PrismaDeviceRepository implements IDeviceRepository { select: { userId: true } }); + // device not found + if (!current) { + throw new Error("DEVICE_NOT_FOUND"); + } + // only allow unbooking of device if userIds match AND account type is not admin or owner if (accountType !== 'ADMIN' && accountType !== 'OWNER' && current?.userId !== userId) { throw new Error("UNAUTHORIZED"); diff --git a/compose.prod.yaml b/compose.prod.yaml index 865e2e0..68f5176 100644 --- a/compose.prod.yaml +++ b/compose.prod.yaml @@ -14,7 +14,7 @@ services: - postgres_data:/var/lib/postgresql/data backend: - image: breyr/top-backend:1.0.3 + image: breyr/top-backend:1.0.4 container_name: backend environment: DATABASE_URL: postgres://demo:demo@postgres:5432/demo @@ -25,7 +25,7 @@ services: - postgres frontend: - image: breyr/top-frontend:1.0.3 + image: breyr/top-frontend:1.0.4 container_name: frontend ports: - "80:80" @@ -33,7 +33,7 @@ services: - backend interconnect-api: - image: breyr/top-interconnectapi:1.0.3 + image: breyr/top-interconnectapi:1.0.4 container_name: interconnect-api environment: SECRET_KEY: your_secret diff --git a/frontend/src/components/reactflow/overlayui/CreateLinkModal.tsx b/frontend/src/components/reactflow/overlayui/CreateLinkModal.tsx index ac92688..bbfc067 100644 --- a/frontend/src/components/reactflow/overlayui/CreateLinkModal.tsx +++ b/frontend/src/components/reactflow/overlayui/CreateLinkModal.tsx @@ -16,7 +16,7 @@ interface CreateLinkModalProps { export default function CreateLinkModal({ deviceData, currentDevicePorts, labDevices, onClose }: CreateLinkModalProps) { const { user } = useAuth(); - const { getEdges } = useReactFlow, Edge>(); + const { getEdges, getNodes } = useReactFlow, Edge>(); const { createLink } = useLinkOperations(); const [selectedFirstDevice, setSelectedFirstDevice] = useState(deviceData?.name ?? ""); const [selectedFirstDevicePort, setSelectedFirstDevicePort] = useState(""); @@ -101,6 +101,12 @@ export default function CreateLinkModal({ deviceData, currentDevicePorts, labDev } }, [selectedSecondDevice, getEdges, labDevices]); + const deviceOnTopology = (deviceName: string) => { + const nodes = getNodes(); + const hasDevice = nodes.filter(d => d.data.deviceData?.name === deviceName); + return hasDevice.length > 0; // Return true if device exists on topology + } + return (
@@ -121,7 +127,7 @@ export default function CreateLinkModal({ deviceData, currentDevicePorts, labDev disabled={!!deviceData?.name} > - {labDevices.filter((device) => device.userId == null || device.userId == user?.id).map((device) => { + {labDevices.filter((device) => (device.userId == null || device.userId == user?.id) && deviceOnTopology(device.name)).map((device) => { const portsArray = device.ports.split(','); const generatedPorts = portsArray.flatMap(portDef => generatePorts(portDef)); const hasAvailablePorts = generatedPorts.some(port => !firstDeviceOccupiedPorts.includes(port)); @@ -157,7 +163,7 @@ export default function CreateLinkModal({ deviceData, currentDevicePorts, labDev className="block w-full mt-1 rounded-md bg-[#ffffff] focus:outline-none" > - {labDevices.filter((d) => d.name !== deviceData?.name).map((device) => { + {labDevices.filter((d) => d.name !== deviceData?.name && deviceOnTopology(d.name)).map((device) => { const portsArray = device.ports.split(','); const generatedPorts = portsArray.flatMap(portDef => generatePorts(portDef)); const occupiedPorts = secondDeviceOccupiedPorts[device.name] || []; diff --git a/frontend/src/hooks/useLinkOperations.ts b/frontend/src/hooks/useLinkOperations.ts index 211450e..98c7361 100644 --- a/frontend/src/hooks/useLinkOperations.ts +++ b/frontend/src/hooks/useLinkOperations.ts @@ -23,7 +23,7 @@ export function useLinkOperationsBase() { const fetchConnectionDetails = async (deviceName: string, devicePort: string) => { const conns = await authenticatedApiClient.getConnectionsByDeviceName(deviceName); return conns.data?.find(c => c.labDevicePort === devicePort); - } + }; // get interconnect information const fetchInterconnectDevice = async (connectionInfo: Connection) => { @@ -69,6 +69,21 @@ export function useLinkOperationsBase() { return devicePort; }; + const checkDevicesExist = async (firstDeviceName: string, secondDeviceName: string): Promise => { + try { + const devices = await authenticatedApiClient.getAllDevices(); + const firstDeviceMatches = devices.data?.filter(d => d.name === firstDeviceName) || []; + const secondDeviceMatches = devices.data?.filter(d => d.name === secondDeviceName) || []; + + // Return true if one or both devices don't exist + return firstDeviceMatches.length === 0 || secondDeviceMatches.length === 0; + } catch (error) { + // In case of API error, assume devices don't exist for safety + console.error("Error checking device existence:", error); + return true; + } + }; + // API operations without ReactFlow dependencies const performLinkOperation = async (params: LinkOperationParams, operation: 'create' | 'delete', createToastPerLink: boolean = true) => { const { firstDeviceName, firstDevicePort, secondDeviceName, secondDevicePort } = params; @@ -103,6 +118,13 @@ export function useLinkOperationsBase() { return false; } + // Check to see if the devices exist, if one or both don't anymore + // just show the Toast as a success and do not perform the interconnect configs + const devicesNotFound = await checkDevicesExist(firstDeviceName, secondDeviceName); + if (devicesNotFound) { + return true; // return true to indicate success + } + // Prepare link payload // Get the correct interconnect information based on the device number for the interconnect const [interconnect1, interconnect2] = firstInterconnectInfo?.deviceNumber === 1