From 830d0ef4ab9af92679aa093a611019804c8b1b78 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Wed, 17 Apr 2024 10:58:19 -0700 Subject: [PATCH 001/191] changes for local dev w/ npm --- local/feeds/.env.local | 3 ++- local/gwa-api/.env.local | 3 ++- local/oauth2-proxy/oauth2-proxy-local.cfg | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/local/feeds/.env.local b/local/feeds/.env.local index c9eb7c447..e7bbf027b 100644 --- a/local/feeds/.env.local +++ b/local/feeds/.env.local @@ -1,5 +1,6 @@ WORKING_PATH=/tmp -DESTINATION_URL=http://apsportal.localtest.me:3000 +# DESTINATION_URL=http://apsportal.localtest.me:3000 +DESTINATION_URL=http://172.100.100.01:3000 KONG_ADMIN_URL=http://kong.localtest.me:8001 CKAN_URL=https://catalog.data.gov.bc.ca LOG_FEEDS=false \ No newline at end of file diff --git a/local/gwa-api/.env.local b/local/gwa-api/.env.local index 7cf8877d2..e71ac0413 100644 --- a/local/gwa-api/.env.local +++ b/local/gwa-api/.env.local @@ -16,7 +16,8 @@ KC_RES_SVR_CLIENT_ID=gwa-api KC_RES_SVR_CLIENT_SECRET=18900468-3db1-43f7-a8af-e75f079eb742 NSP_ENABLED=false PROTECTED_KUBE_NAMESPACES= -PORTAL_ACTIVITY_URL=http://apsportal.localtest.me:3000 +# PORTAL_ACTIVITY_URL=http://apsportal.localtest.me:3000 +PORTAL_ACTIVITY_URL=http://172.100.100.01:3000 PORTAL_ACTIVITY_TOKEN= HOST_TRANSFORM_ENABLED=false HOST_TRANSFORM_BASE_URL= diff --git a/local/oauth2-proxy/oauth2-proxy-local.cfg b/local/oauth2-proxy/oauth2-proxy-local.cfg index 427904629..4b7a9d557 100644 --- a/local/oauth2-proxy/oauth2-proxy-local.cfg +++ b/local/oauth2-proxy/oauth2-proxy-local.cfg @@ -23,7 +23,8 @@ set_authorization_header="false" pass_authorization_header="false" skip_auth_regex="/login|/health|/public|/docs|/redirect|/_next|/images|/devportal|/manager|/about|/maintenance|/admin/session|/ds/api|/gw/api|/feed/|/signout|^[/]$" whitelist_domains="keycloak.localtest.me:9081" -upstreams=["http://apsportal.localtest.me:3000"] +# upstreams=["http://apsportal.localtest.me:3000"] +upstreams=["http://172.25.20.31:3000"] skip_provider_button='true' redis_connection_url="redis://redis-master:6379" session_store_type='redis' From 25011f8b2c0e7e10f6455826e85be47fd41f900b Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Mon, 22 Apr 2024 10:29:14 -0700 Subject: [PATCH 002/191] new page for no gateways - WIP --- .../pages/manager/namespaces/index.tsx | 98 ++++++++++++++---- src/nextapp/public/images/empty_folder.png | Bin 0 -> 10145 bytes 2 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 src/nextapp/public/images/empty_folder.png diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index 991053adf..c0a064ec8 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -31,11 +31,14 @@ import { useAuth } from '@/shared/services/auth'; import { FaBuilding, FaChartBar, + FaChartLine, FaCheckCircle, FaChevronRight, FaClock, FaExternalLinkAlt, FaInfoCircle, + FaLock, + FaSearch, FaShieldAlt, FaTrash, FaUserAlt, @@ -48,6 +51,7 @@ import { RiApps2Fill } from 'react-icons/ri'; import PreviewBanner from '@/components/preview-banner'; import { useQueryClient } from 'react-query'; import { useRouter } from 'next/router'; +import Card from '@/components/card' import EmptyPane from '@/components/empty-pane'; import NamespaceMenu from '@/components/namespace-menu/namespace-menu'; import NewNamespace from '@/components/new-namespace'; @@ -246,25 +250,81 @@ const NamespacesPage: React.FC = () => { {!hasNamespace && ( - - - - or - - - - + <> + + + Empty folder + No gateways created yet + + What is a gateway? + + Gateways act as a central entry point for multiple APIs. Its main purpose is to facilitate communication and control the data flow between your APIs and those who consume them. + + + After your first gateway is created, in this section you can do things like: + + + + + + + Make your products discoverable + + + + Allow citizens to find your APIs on the BC Government API Directory. + + + + + + + Control access to your products + + + + Decide who can use the data and how they access it. + + + + + + + View usage metrics + + + + Get a fast overview of how much and how often the services you've set up are being used. + + + + + + + + or + + + + + )} {hasNamespace && ( diff --git a/src/nextapp/public/images/empty_folder.png b/src/nextapp/public/images/empty_folder.png new file mode 100644 index 0000000000000000000000000000000000000000..30b0bf5d539e246548dec90edb32f6fb83017853 GIT binary patch literal 10145 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 literal 0 HcmV?d00001 From 160081a4093d7d781a7c12cafadc67402ff45347 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Mon, 22 Apr 2024 15:52:18 -0700 Subject: [PATCH 003/191] CliCommand component --- .../components/cli-command/cli-command.tsx | 59 +++++ src/nextapp/components/cli-command/index.ts | 1 + .../pages/manager/namespaces/index.tsx | 241 +++++++++++++++++- src/nextapp/public/images/glossary_search.png | Bin 0 -> 13295 bytes 4 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 src/nextapp/components/cli-command/cli-command.tsx create mode 100644 src/nextapp/components/cli-command/index.ts create mode 100644 src/nextapp/public/images/glossary_search.png diff --git a/src/nextapp/components/cli-command/cli-command.tsx b/src/nextapp/components/cli-command/cli-command.tsx new file mode 100644 index 000000000..f94be47bd --- /dev/null +++ b/src/nextapp/components/cli-command/cli-command.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { + Box, + Button, + Flex, + Icon, + IconButton, + Input, + Text, + Tooltip, + useToast, +} from '@chakra-ui/react'; +import { IoCopy } from 'react-icons/io5'; + +interface CliCommandProps { + value: string; +} + +const CliCommand: React.FC = ({ value }) => { + const toast = useToast(); + const handleClipboard = React.useCallback( + (text: string) => async () => { + await navigator.clipboard.writeText(text); + toast({ + title: 'Copied to clipboard!', + status: 'success', + }); + }, + [toast] + ); + + return ( + + $ {value} + + } + fontSize='20px' + onClick={handleClipboard(value)} + /> + + + ); +}; + +export default CliCommand; diff --git a/src/nextapp/components/cli-command/index.ts b/src/nextapp/components/cli-command/index.ts new file mode 100644 index 000000000..27935a223 --- /dev/null +++ b/src/nextapp/components/cli-command/index.ts @@ -0,0 +1 @@ +export { default } from './cli-command'; diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index c0a064ec8..081591947 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -9,19 +9,20 @@ import { Text, Grid, GridItem, + HStack, Icon, Link, useToast, - VStack, useDisclosure, PopoverTrigger, Popover, PopoverContent, PopoverArrow, PopoverBody, - IconButton, Skeleton, Tooltip, + VStack, + Stack } from '@chakra-ui/react'; import ConfirmationDialog from '@/components/confirmation-dialog'; import Head from 'next/head'; @@ -52,6 +53,7 @@ import PreviewBanner from '@/components/preview-banner'; import { useQueryClient } from 'react-query'; import { useRouter } from 'next/router'; import Card from '@/components/card' +import CliCommand from '@/components/cli-command' import EmptyPane from '@/components/empty-pane'; import NamespaceMenu from '@/components/namespace-menu/namespace-menu'; import NewNamespace from '@/components/new-namespace'; @@ -305,6 +307,241 @@ const NamespacesPage: React.FC = () => { + + + Steps to create and configure your first gateway + + Follow these steps to create and configure your first gateway in a test/training instance. For full documentation on how to set up an API, consult our{' '} + 'API Provider Quick Start' + {' '}guide. + + + + + + + + + Glossary search + + + + Download our Command Line Interface (CLI) + + + Our CLI is a convenient way to configure your gateways.{' '} + Download it here. + + + + + + + + + Glossary search + + + + Prepare the configuration + + + Use our template Yaml file to create a gateway and set up its configuration: services, routes and plugins. + Download it here. + + + + + + + + + Glossary search + + + + Apply configuration to your gateway + + + Run the sync command in the CLI to apply your configuration to your gateway. + Download it here. + + + + + + Glossary search + + New terminology? + + + + Explore our glossary + + {' '}for easy-to-understand definitions + + + + GWA CLI commands + + These useful commands will save you time and help you manage your gateway resources in a more efficient way. For more details visit our {' '} + GWA CLI commands + {' '}section. + + + + Prepare the configuration + + Log in + Login via device with your IDIR. + + + + Create gateway + Generate a new gateway with this command. The new gateway will hav assigned an automatic alphanumeric ID + and a generic display name that can be modified to easily idenityf and distinguish this gateway from others. + + + + + + + + PyA07*naRCr$Pod-#);lFYpIeD7)B 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 tfMj>Hp$GiF|F0u~-+}sd Date: Mon, 22 Apr 2024 15:59:13 -0700 Subject: [PATCH 004/191] restore IconButton --- src/nextapp/pages/manager/namespaces/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index 081591947..83c5dccbb 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -11,6 +11,7 @@ import { GridItem, HStack, Icon, + IconButton, Link, useToast, useDisclosure, From 413fe49b43275430890d30a91ee3af324baef402 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Tue, 23 Apr 2024 09:14:58 -0700 Subject: [PATCH 005/191] create all elements --- .../components/cli-command/cli-command.tsx | 51 ++++--- .../pages/manager/namespaces/index.tsx | 135 +++++++++++------- src/nextapp/public/images/api_apply.png | Bin 0 -> 53207 bytes src/nextapp/public/images/config.png | Bin 0 -> 29183 bytes src/nextapp/public/images/download.png | Bin 0 -> 23416 bytes 5 files changed, 115 insertions(+), 71 deletions(-) create mode 100644 src/nextapp/public/images/api_apply.png create mode 100644 src/nextapp/public/images/config.png create mode 100644 src/nextapp/public/images/download.png diff --git a/src/nextapp/components/cli-command/cli-command.tsx b/src/nextapp/components/cli-command/cli-command.tsx index f94be47bd..08504b7fb 100644 --- a/src/nextapp/components/cli-command/cli-command.tsx +++ b/src/nextapp/components/cli-command/cli-command.tsx @@ -3,6 +3,7 @@ import { Box, Button, Flex, + Heading, Icon, IconButton, Input, @@ -11,12 +12,16 @@ import { useToast, } from '@chakra-ui/react'; import { IoCopy } from 'react-icons/io5'; +import { description } from 'casual-browserify'; interface CliCommandProps { - value: string; + id?: string; + title: string; + description: string; + command: string; } -const CliCommand: React.FC = ({ value }) => { +const CliCommand: React.FC = ({ id, title, description, command }) => { const toast = useToast(); const handleClipboard = React.useCallback( (text: string) => async () => { @@ -30,29 +35,41 @@ const CliCommand: React.FC = ({ value }) => { ); return ( - - $ {value} - - } - fontSize='20px' - onClick={handleClipboard(value)} - /> - + {title} + {description} + + $ {command} + + } + fontSize='20px' + onClick={handleClipboard(command)} + /> + + + ); }; diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index 83c5dccbb..314a118c5 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -349,9 +349,9 @@ const NamespacesPage: React.FC = () => { transform="translate(-50%, -50%)" > Glossary search @@ -399,9 +399,9 @@ const NamespacesPage: React.FC = () => { transform="translate(-50%, -50%)" > Glossary search @@ -411,15 +411,19 @@ const NamespacesPage: React.FC = () => { Prepare the configuration - Use our template Yaml file to create a gateway and set up its configuration: services, routes and plugins. + Use our{' '} Download it here. + >template Yaml file + {' '}to{' '} + create a gateway + {' '}and set up its configuration: services, routes and plugins. @@ -449,9 +453,9 @@ const NamespacesPage: React.FC = () => { transform="translate(-50%, -50%)" > Glossary search @@ -461,15 +465,13 @@ const NamespacesPage: React.FC = () => { Apply configuration to your gateway - Run the sync command in the CLI to apply your configuration to your gateway. - Download it here. + Run the{' '} + apply command + {' '}in the CLI to apply your configuration to your gateway. @@ -499,7 +501,7 @@ const NamespacesPage: React.FC = () => { GWA CLI commands - + These useful commands will save you time and help you manage your gateway resources in a more efficient way. For more details visit our {' '} { {' '}section. - - Prepare the configuration - - Log in - Login via device with your IDIR. - - - - Create gateway - Generate a new gateway with this command. The new gateway will hav assigned an automatic alphanumeric ID - and a generic display name that can be modified to easily idenityf and distinguish this gateway from others. - - + + Prepare the configuration + + - - + Apply configuration to your gateway + + + + Help + + + Other utility functions + + + diff --git a/src/nextapp/public/images/api_apply.png b/src/nextapp/public/images/api_apply.png new file mode 100644 index 0000000000000000000000000000000000000000..b642db9d80df26ecc4e586b2ce8045687eaf7e12 GIT binary patch literal 53207 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 B9-#mL literal 0 HcmV?d00001 diff --git a/src/nextapp/public/images/download.png b/src/nextapp/public/images/download.png new file mode 100644 index 0000000000000000000000000000000000000000..7a402aab73de3be32aa9c4dcddb443cfc0e074ca GIT binary patch literal 23416 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�#YSA~0{JC-A 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 AdH?_b literal 0 HcmV?d00001 From e43202195ea662cf628e22623fe244bf455c3f86 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Tue, 23 Apr 2024 09:34:45 -0700 Subject: [PATCH 006/191] smooth scroll --- src/nextapp/pages/manager/namespaces/index.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index 314a118c5..5dacf8b82 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -240,6 +240,13 @@ const NamespacesPage: React.FC = () => { ); + const handleSmoothScroll = (targetId) => { + const element = document.getElementById(targetId); + if (element) { + element.scrollIntoView({ behavior: 'smooth' }); + } + }; + return ( <> @@ -416,6 +423,7 @@ const NamespacesPage: React.FC = () => { href={"#generate-config"} color="bc-link" textDecor="underline" + onClick={() => handleSmoothScroll('generate-config')} >template Yaml file {' '}to{' '} Date: Tue, 23 Apr 2024 11:07:04 -0700 Subject: [PATCH 007/191] fix scroll --- .../components/cli-command/cli-command.tsx | 8 +- .../pages/manager/namespaces/index.tsx | 78 +++++++++++++++---- 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/nextapp/components/cli-command/cli-command.tsx b/src/nextapp/components/cli-command/cli-command.tsx index 08504b7fb..177b416f6 100644 --- a/src/nextapp/components/cli-command/cli-command.tsx +++ b/src/nextapp/components/cli-command/cli-command.tsx @@ -1,18 +1,13 @@ import * as React from 'react'; import { Box, - Button, - Flex, Heading, - Icon, IconButton, - Input, Text, Tooltip, useToast, } from '@chakra-ui/react'; import { IoCopy } from 'react-icons/io5'; -import { description } from 'casual-browserify'; interface CliCommandProps { id?: string; @@ -68,8 +63,7 @@ const CliCommand: React.FC = ({ id, title, description, command /> - - + ); }; diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index 5dacf8b82..8be294d3c 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { useEffect } from 'react'; import ApproveBanner from '@/components/approve-banner'; import { Button, @@ -118,6 +118,58 @@ const secondaryActions = [ }, ]; +// function SmoothScrollLink({ href, children }) { +// useEffect(() => { +// const handleClick = (event) => { +// event.preventDefault(); +// const targetId = href.substring(1); // Remove the # from href to get the target id +// const target = document.getElementById(targetId); +// if (target) { +// const yOffset = -50; // You may adjust this value to offset the scroll position if needed +// const y = target.getBoundingClientRect().top + window.pageYOffset + yOffset; +// window.scrollTo({ top: y, behavior: "smooth" }); +// } +// }; + +// const anchorLinks = document.querySelectorAll('a[href^="#"]'); +// anchorLinks.forEach((link) => { +// link.addEventListener("click", handleClick); +// }); + +// return () => { +// anchorLinks.forEach((link) => { +// link.removeEventListener("click", handleClick); +// }); +// }; +// }, [href]); + +// return {children}; +// } + +function SmoothScrollLink({ href, children }) { + const handleClick = (event) => { + event.preventDefault(); + const targetId = href.substring(1); // Remove the # from href to get the target id + const target = document.getElementById(targetId); + if (target) { + const yOffset = -120; // You may adjust this value to offset the scroll position if needed + const y = target.getBoundingClientRect().top + window.pageYOffset + yOffset; + window.scrollTo({ top: y, behavior: "smooth" }); + } + }; + + return ( + + {children} + + ) +} + const NamespacesPage: React.FC = () => { const { user } = useAuth(); const hasNamespace = !!user?.namespace; @@ -258,7 +310,7 @@ const NamespacesPage: React.FC = () => { - + {!hasNamespace && ( <> @@ -419,18 +471,13 @@ const NamespacesPage: React.FC = () => { Use our{' '} - handleSmoothScroll('generate-config')} - >template Yaml file + >template Yaml file {' '}to{' '} - create a gateway + >create a gateway {' '}and set up its configuration: services, routes and plugins. @@ -474,11 +521,9 @@ const NamespacesPage: React.FC = () => { Run the{' '} - apply command + apply command {' '}in the CLI to apply your configuration to your gateway. @@ -578,6 +623,7 @@ const NamespacesPage: React.FC = () => { + {/* this element needs to get moved once logic is revised */} Date: Thu, 25 Apr 2024 09:37:57 -0700 Subject: [PATCH 008/191] move no-gateways content into new component --- .../components/cli-command/cli-command.tsx | 4 +- .../gateway-get-started.tsx | 390 ++++++++++++++++++ .../components/gateway-get-started/index.ts | 1 + .../pages/manager/namespaces/index.tsx | 373 +---------------- 4 files changed, 396 insertions(+), 372 deletions(-) create mode 100644 src/nextapp/components/gateway-get-started/gateway-get-started.tsx create mode 100644 src/nextapp/components/gateway-get-started/index.ts diff --git a/src/nextapp/components/cli-command/cli-command.tsx b/src/nextapp/components/cli-command/cli-command.tsx index 177b416f6..19fd16b75 100644 --- a/src/nextapp/components/cli-command/cli-command.tsx +++ b/src/nextapp/components/cli-command/cli-command.tsx @@ -12,7 +12,7 @@ import { IoCopy } from 'react-icons/io5'; interface CliCommandProps { id?: string; title: string; - description: string; + description: React.ReactNode; command: string; } @@ -52,7 +52,7 @@ const CliCommand: React.FC = ({ id, title, description, command alignItems="center" justifyContent="space-between" > - $ {command} + $ {command} { + event.preventDefault(); + const targetId = href.substring(1); // Remove the # from href to get the target id + const target = document.getElementById(targetId); + if (target) { + const yOffset = -120; // You may adjust this value to offset the scroll position if needed + const y = target.getBoundingClientRect().top + window.pageYOffset + yOffset; + window.scrollTo({ top: y, behavior: "smooth" }); + } + }; + + return ( + + {children} + + ) +} + +const GatewayGetStarted: React.FC = () => { + const global = useGlobal(); + + return ( + <> + + + Empty folder + No gateways created yet + + What is a gateway? + + Gateways act as a central entry point for multiple APIs. Its main purpose is to facilitate communication and control the data flow between your APIs and those who consume them. + + + After your first gateway is created, in this section you can do things like: + + + + + + + Make your products discoverable + + + + Allow citizens to find your APIs on the BC Government API Directory. + + + + + + + Control access to your products + + + + Decide who can use the data and how they access it. + + + + + + + View usage metrics + + + + Get a fast overview of how much and how often the services you've set up are being used. + + + + + + + Steps to create and configure your first gateway + + Follow these steps to create and configure your first gateway in a test/training instance. For a more detailed version of this tutorial, consult our{' '} + 'API Provider Quick Start' + {' '}tutorial. + + + + + + + + + Download + + + + Download our Command Line Interface (CLI) + + + Our CLI is a convenient way to configure your gateways.{' '} + Download it here. + + + + + + + + + Configuration + + + + Prepare your configuration + + + Use our{' '} + template Yaml file + {' '}to{' '} + create a gateway + {' '}and set up its configuration: services, routes and plugins. + + + + + + + + + Apply configuration + + + + Apply configuration to your gateway + + + Run the{' '} + apply command + {' '}in the CLI to apply your configuration to your gateway. + + + + + + Glossary search + + New terminology? + + + + Explore our glossary + + {' '}for easy-to-understand definitions + + + + GWA CLI commands + + These useful commands will save you time and help you manage your gateway resources in a more efficient way. For more details visit our {' '} + GWA CLI commands + {' '}section. + + + + Prepare your configuration + + + + + Apply configuration to your gateway + + + {/* TODO: add anchor link to end of tutorial */} + + Congratulations! You've set up X, Y, and Z. + For more information on accessing your gateway and next steps, visit the{' '} + tutorial. + + + + {/* TODO: the following requires revision */} + + Help + + If you are not sure about how to use a specific command, you can type --help after the + command's name to learn more about its usage and syntax. + + } + command='gwa --help' + /> + + Other utility functions + + + + + + + ); +}; + +export default GatewayGetStarted; diff --git a/src/nextapp/components/gateway-get-started/index.ts b/src/nextapp/components/gateway-get-started/index.ts new file mode 100644 index 000000000..92a469558 --- /dev/null +++ b/src/nextapp/components/gateway-get-started/index.ts @@ -0,0 +1 @@ +export { default } from './gateway-get-started'; diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index 8be294d3c..40485b80e 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -54,7 +54,7 @@ import PreviewBanner from '@/components/preview-banner'; import { useQueryClient } from 'react-query'; import { useRouter } from 'next/router'; import Card from '@/components/card' -import CliCommand from '@/components/cli-command' +import GatewayGetStarted from '@/components/gateway-get-started' import EmptyPane from '@/components/empty-pane'; import NamespaceMenu from '@/components/namespace-menu/namespace-menu'; import NewNamespace from '@/components/new-namespace'; @@ -118,58 +118,6 @@ const secondaryActions = [ }, ]; -// function SmoothScrollLink({ href, children }) { -// useEffect(() => { -// const handleClick = (event) => { -// event.preventDefault(); -// const targetId = href.substring(1); // Remove the # from href to get the target id -// const target = document.getElementById(targetId); -// if (target) { -// const yOffset = -50; // You may adjust this value to offset the scroll position if needed -// const y = target.getBoundingClientRect().top + window.pageYOffset + yOffset; -// window.scrollTo({ top: y, behavior: "smooth" }); -// } -// }; - -// const anchorLinks = document.querySelectorAll('a[href^="#"]'); -// anchorLinks.forEach((link) => { -// link.addEventListener("click", handleClick); -// }); - -// return () => { -// anchorLinks.forEach((link) => { -// link.removeEventListener("click", handleClick); -// }); -// }; -// }, [href]); - -// return {children}; -// } - -function SmoothScrollLink({ href, children }) { - const handleClick = (event) => { - event.preventDefault(); - const targetId = href.substring(1); // Remove the # from href to get the target id - const target = document.getElementById(targetId); - if (target) { - const yOffset = -120; // You may adjust this value to offset the scroll position if needed - const y = target.getBoundingClientRect().top + window.pageYOffset + yOffset; - window.scrollTo({ top: y, behavior: "smooth" }); - } - }; - - return ( - - {children} - - ) -} - const NamespacesPage: React.FC = () => { const { user } = useAuth(); const hasNamespace = !!user?.namespace; @@ -292,13 +240,6 @@ const NamespacesPage: React.FC = () => { ); - const handleSmoothScroll = (targetId) => { - const element = document.getElementById(targetId); - if (element) { - element.scrollIntoView({ behavior: 'smooth' }); - } - }; - return ( <> @@ -313,317 +254,9 @@ const NamespacesPage: React.FC = () => { {!hasNamespace && ( <> - - - Empty folder - No gateways created yet - - What is a gateway? - - Gateways act as a central entry point for multiple APIs. Its main purpose is to facilitate communication and control the data flow between your APIs and those who consume them. - - - After your first gateway is created, in this section you can do things like: - - - - - - - Make your products discoverable - - - - Allow citizens to find your APIs on the BC Government API Directory. - - - - - - - Control access to your products - - - - Decide who can use the data and how they access it. - - - - - - - View usage metrics - - - - Get a fast overview of how much and how often the services you've set up are being used. - - - - - - - Steps to create and configure your first gateway - - Follow these steps to create and configure your first gateway in a test/training instance. For full documentation on how to set up an API, consult our{' '} - 'API Provider Quick Start' - {' '}guide. - - - - - - - - - Download - - - - Download our Command Line Interface (CLI) - - - Our CLI is a convenient way to configure your gateways.{' '} - Download it here. - - - - - - - - - Configuration - - - - Prepare the configuration - - - Use our{' '} - template Yaml file - {' '}to{' '} - create a gateway - {' '}and set up its configuration: services, routes and plugins. - - - - - - - - - Apply configuration - - - - Apply configuration to your gateway - - - Run the{' '} - apply command - {' '}in the CLI to apply your configuration to your gateway. - - - - - - Glossary search - - New terminology? - - - - Explore our glossary - - {' '}for easy-to-understand definitions - - - - GWA CLI commands - - These useful commands will save you time and help you manage your gateway resources in a more efficient way. For more details visit our {' '} - GWA CLI commands - {' '}section. - - - - Prepare the configuration - - - - Apply configuration to your gateway - - + - Help - - - Other utility functions - - - - - - - {/* this element needs to get moved once logic is revised */} + {/* this element needs to get moved or removed once logic is revised */} Date: Thu, 25 Apr 2024 11:17:28 -0700 Subject: [PATCH 009/191] add query to see if there are gateways --- ...ocaltestme4180managernamespaces-9dd903.gql | 5 ++ ...ocaltestme4180managernamespaces-f028bd.gql | 6 +++ src/controllers/v2/openapi.yaml | 52 ------------------- src/controllers/v2/routes.ts | 27 ---------- src/controllers/v2/types.ts | 1 - .../pages/manager/namespaces/index.tsx | 26 ++++++++-- 6 files changed, 34 insertions(+), 83 deletions(-) create mode 100644 src/authz/graphql-whitelist/httpoauth2proxylocaltestme4180managernamespaces-9dd903.gql create mode 100644 src/authz/graphql-whitelist/httpoauth2proxylocaltestme4180managernamespaces-f028bd.gql diff --git a/src/authz/graphql-whitelist/httpoauth2proxylocaltestme4180managernamespaces-9dd903.gql b/src/authz/graphql-whitelist/httpoauth2proxylocaltestme4180managernamespaces-9dd903.gql new file mode 100644 index 000000000..598134fb2 --- /dev/null +++ b/src/authz/graphql-whitelist/httpoauth2proxylocaltestme4180managernamespaces-9dd903.gql @@ -0,0 +1,5 @@ + + query GetNamespaces { + allNamespaces { + } + } diff --git a/src/authz/graphql-whitelist/httpoauth2proxylocaltestme4180managernamespaces-f028bd.gql b/src/authz/graphql-whitelist/httpoauth2proxylocaltestme4180managernamespaces-f028bd.gql new file mode 100644 index 000000000..36abce8e3 --- /dev/null +++ b/src/authz/graphql-whitelist/httpoauth2proxylocaltestme4180managernamespaces-f028bd.gql @@ -0,0 +1,6 @@ + + query GetNamespaces { + allNamespaces { + name + } + } diff --git a/src/controllers/v2/openapi.yaml b/src/controllers/v2/openapi.yaml index 6696f64e2..f196df531 100644 --- a/src/controllers/v2/openapi.yaml +++ b/src/controllers/v2/openapi.yaml @@ -554,54 +554,6 @@ components: additionalProperties: false DraftDatasetRefID: type: string - LegalRefID: - type: string - CredentialIssuerRefID: - type: string - Environment: - properties: - appId: - type: string - name: - type: string - enum: - - dev - - test - - prod - - sandbox - - other - active: - type: boolean - approval: - type: boolean - flow: - type: string - enum: - - public - - protected-externally - - authorization-code - - client-credentials - - kong-acl-only - - kong-api-key-only - - kong-api-key-acl - additionalDetailsToRequest: - type: string - services: - items: - $ref: '#/components/schemas/GatewayServiceRefID' - type: array - legal: - $ref: '#/components/schemas/LegalRefID' - credentialIssuer: - $ref: '#/components/schemas/CredentialIssuerRefID' - type: object - additionalProperties: false - example: - name: dev - active: false - approval: false - flow: public - appId: '00000000' Product: properties: appId: @@ -614,10 +566,6 @@ components: type: string dataset: $ref: '#/components/schemas/DraftDatasetRefID' - environments: - items: - $ref: '#/components/schemas/Environment' - type: array type: object additionalProperties: false example: diff --git a/src/controllers/v2/routes.ts b/src/controllers/v2/routes.ts index 4d6561ced..e4c115c03 100644 --- a/src/controllers/v2/routes.ts +++ b/src/controllers/v2/routes.ts @@ -362,32 +362,6 @@ const models: TsoaRoute.Models = { "type": {"dataType":"string","validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "LegalRefID": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "CredentialIssuerRefID": { - "dataType": "refAlias", - "type": {"dataType":"string","validators":{}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Environment": { - "dataType": "refObject", - "properties": { - "appId": {"dataType":"string"}, - "name": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["dev"]},{"dataType":"enum","enums":["test"]},{"dataType":"enum","enums":["prod"]},{"dataType":"enum","enums":["sandbox"]},{"dataType":"enum","enums":["other"]}]}, - "active": {"dataType":"boolean"}, - "approval": {"dataType":"boolean"}, - "flow": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["public"]},{"dataType":"enum","enums":["protected-externally"]},{"dataType":"enum","enums":["authorization-code"]},{"dataType":"enum","enums":["client-credentials"]},{"dataType":"enum","enums":["kong-acl-only"]},{"dataType":"enum","enums":["kong-api-key-only"]},{"dataType":"enum","enums":["kong-api-key-acl"]}]}, - "additionalDetailsToRequest": {"dataType":"string"}, - "services": {"dataType":"array","array":{"dataType":"refAlias","ref":"GatewayServiceRefID"}}, - "legal": {"ref":"LegalRefID"}, - "credentialIssuer": {"ref":"CredentialIssuerRefID"}, - }, - "additionalProperties": false, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Product": { "dataType": "refObject", "properties": { @@ -396,7 +370,6 @@ const models: TsoaRoute.Models = { "description": {"dataType":"string"}, "namespace": {"dataType":"string"}, "dataset": {"ref":"DraftDatasetRefID"}, - "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, }, "additionalProperties": false, }, diff --git a/src/controllers/v2/types.ts b/src/controllers/v2/types.ts index a5ed8e61e..1d061cbda 100644 --- a/src/controllers/v2/types.ts +++ b/src/controllers/v2/types.ts @@ -281,7 +281,6 @@ export interface Product { description?: string; namespace?: string; dataset?: DraftDatasetRefID; - environments?: Environment[]; } diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index 40485b80e..352719e6a 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -48,7 +48,7 @@ import { FaUserShield, } from 'react-icons/fa'; import { gql } from 'graphql-request'; -import { restApi, useApiMutation } from '@/shared/services/api'; +import { restApi, useApiMutation, useApi } from '@/shared/services/api'; import { RiApps2Fill } from 'react-icons/ri'; import PreviewBanner from '@/components/preview-banner'; import { useQueryClient } from 'react-query'; @@ -128,6 +128,12 @@ const NamespacesPage: React.FC = () => { const namespace = useCurrentNamespace(); const { isOpen, onClose, onOpen } = useDisclosure(); const global = useGlobal(); + const { data, isLoading, isSuccess, isError } = useApi( + 'allNamespaces', + { query }, + { suspense: false } + ); + console.log(data) const currentOrg = React.useMemo(() => { if (namespace.isSuccess && namespace.data.currentNamespace?.org) { return { @@ -252,10 +258,16 @@ const NamespacesPage: React.FC = () => { + <> + {isError && ( + Gateways Failed to Load + )} + {isSuccess && data.allNamespaces.length == 0 && ( + + )} + {!hasNamespace && ( <> - - {/* this element needs to get moved or removed once logic is revised */} Date: Wed, 17 Apr 2024 10:58:19 -0700 Subject: [PATCH 010/191] changes for local dev w/ npm --- local/feeds/.env.local | 3 ++- local/gwa-api/.env.local | 3 ++- local/oauth2-proxy/oauth2-proxy-local.cfg | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/local/feeds/.env.local b/local/feeds/.env.local index c9eb7c447..e7bbf027b 100644 --- a/local/feeds/.env.local +++ b/local/feeds/.env.local @@ -1,5 +1,6 @@ WORKING_PATH=/tmp -DESTINATION_URL=http://apsportal.localtest.me:3000 +# DESTINATION_URL=http://apsportal.localtest.me:3000 +DESTINATION_URL=http://172.100.100.01:3000 KONG_ADMIN_URL=http://kong.localtest.me:8001 CKAN_URL=https://catalog.data.gov.bc.ca LOG_FEEDS=false \ No newline at end of file diff --git a/local/gwa-api/.env.local b/local/gwa-api/.env.local index 7cf8877d2..e71ac0413 100644 --- a/local/gwa-api/.env.local +++ b/local/gwa-api/.env.local @@ -16,7 +16,8 @@ KC_RES_SVR_CLIENT_ID=gwa-api KC_RES_SVR_CLIENT_SECRET=18900468-3db1-43f7-a8af-e75f079eb742 NSP_ENABLED=false PROTECTED_KUBE_NAMESPACES= -PORTAL_ACTIVITY_URL=http://apsportal.localtest.me:3000 +# PORTAL_ACTIVITY_URL=http://apsportal.localtest.me:3000 +PORTAL_ACTIVITY_URL=http://172.100.100.01:3000 PORTAL_ACTIVITY_TOKEN= HOST_TRANSFORM_ENABLED=false HOST_TRANSFORM_BASE_URL= diff --git a/local/oauth2-proxy/oauth2-proxy-local.cfg b/local/oauth2-proxy/oauth2-proxy-local.cfg index 427904629..4b7a9d557 100644 --- a/local/oauth2-proxy/oauth2-proxy-local.cfg +++ b/local/oauth2-proxy/oauth2-proxy-local.cfg @@ -23,7 +23,8 @@ set_authorization_header="false" pass_authorization_header="false" skip_auth_regex="/login|/health|/public|/docs|/redirect|/_next|/images|/devportal|/manager|/about|/maintenance|/admin/session|/ds/api|/gw/api|/feed/|/signout|^[/]$" whitelist_domains="keycloak.localtest.me:9081" -upstreams=["http://apsportal.localtest.me:3000"] +# upstreams=["http://apsportal.localtest.me:3000"] +upstreams=["http://172.25.20.31:3000"] skip_provider_button='true' redis_connection_url="redis://redis-master:6379" session_store_type='redis' From 54b0d1b1468213eb8c61a60f2b2ffb7d71525f43 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Wed, 1 May 2024 11:18:24 -0700 Subject: [PATCH 011/191] Move ns dropdown --- .../components/auth-action/auth-action.tsx | 6 - .../namespace-menu/namespace-menu.tsx | 61 ++++++--- src/nextapp/components/nav-bar/nav-bar.tsx | 125 +++++++++++------- src/nextapp/pages/_app.tsx | 5 +- .../pages/manager/namespaces/index.tsx | 7 +- 5 files changed, 124 insertions(+), 80 deletions(-) diff --git a/src/nextapp/components/auth-action/auth-action.tsx b/src/nextapp/components/auth-action/auth-action.tsx index db3483799..9bf11837d 100644 --- a/src/nextapp/components/auth-action/auth-action.tsx +++ b/src/nextapp/components/auth-action/auth-action.tsx @@ -24,7 +24,6 @@ import { makeRedirectUrl, useAuth, } from '@/shared/services/auth'; -import NamespaceMenu from '../namespace-menu'; import HelpMenu from './help-menu'; import NextLink from 'next/link'; import { useGlobal } from '@/shared/services/global'; @@ -67,7 +66,6 @@ const Signin: React.FC = ({ site }) => { return ( - = ({ site }) => { return ( - } spacing={4} > - {user.roles.includes('portal-user') && } = ({ }) => { const client = useQueryClient(); const toast = useToast(); + const [search, setSearch] = React.useState(''); const newNamespaceDisclosure = useDisclosure(); const managerDisclosure = useDisclosure(); const { data, isLoading, isSuccess, isError } = useApi( @@ -80,21 +83,32 @@ const NamespaceMenu: React.FC = ({ - {user?.namespace ?? buttonMessage ?? 'No Active Namespace'}{' '} - + + {user?.namespace ?? buttonMessage ?? 'No Active Namespace'} + + = ({ Namespaces Failed to Load )} {isSuccess && data.allNamespaces.length > 0 && ( - + + + + {data.allNamespaces .filter((n) => n.name !== user.namespace) @@ -124,22 +147,26 @@ const NamespaceMenu: React.FC = ({ flexDir="column" alignItems="flex-start" pos="relative" + p={6} > - {differenceInDays(today, new Date(n.orgUpdatedAt)) <= + {/* {differenceInDays(today, new Date(n.orgUpdatedAt)) <= 5 && ( New - )} - {n.name} - { - /* @ts-ignore */ + )} */} + + + {n.name} + + {/* { + // @ts-ignore !n.orgEnabled && ( API Publishing Disabled ) - } + } */} ))} diff --git a/src/nextapp/components/nav-bar/nav-bar.tsx b/src/nextapp/components/nav-bar/nav-bar.tsx index 309a0a907..0c39fad54 100644 --- a/src/nextapp/components/nav-bar/nav-bar.tsx +++ b/src/nextapp/components/nav-bar/nav-bar.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; -import { Box, Container, Link } from '@chakra-ui/react'; +import { Box, Container, Flex, Link, Text} from '@chakra-ui/react'; import NextLink from 'next/link'; import type { NavLink } from '@/shared/data/links'; +import NamespaceMenu from '../namespace-menu'; import { useAuth } from '@/shared/services/auth'; const linkProps = { @@ -46,58 +47,82 @@ const NavBar: React.FC = ({ site, links, pathname }) => { return ''; }, [pathname]); - + return ( - - + - {authenticatedLinks.map(({ BadgeElement, ...link }) => ( - - - - - {link.name} - - {BadgeElement && ( - - + + {authenticatedLinks.map(({ BadgeElement, ...link }) => ( + + + + + {link.name} - )} - - - - ))} - - + {BadgeElement && ( + + + + )} + + + + ))} + + + {(pathname.startsWith('/manager/') || pathname === '/devportal/api-directory/your-products') && ( + + + + Gateway selected: + + + + + )} + + ); }; diff --git a/src/nextapp/pages/_app.tsx b/src/nextapp/pages/_app.tsx index 3e0a4e26b..2a9cd890b 100644 --- a/src/nextapp/pages/_app.tsx +++ b/src/nextapp/pages/_app.tsx @@ -69,6 +69,9 @@ const App: React.FC = ({ Component, pageProps }) => { return 'devportal'; }, [router]); + // Temp solution for handing spacing around new gateways dropdown menu + const gatewaysMenu = (router?.pathname.startsWith('/manager/') || router?.pathname === '/devportal/api-directory/your-products') + if (!queryClientRef.current) { queryClientRef.current = new QueryClient({ defaultOptions: { @@ -105,7 +108,7 @@ const App: React.FC = ({ Component, pageProps }) => { - + diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index 991053adf..9b99f17fa 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -49,7 +49,6 @@ import PreviewBanner from '@/components/preview-banner'; import { useQueryClient } from 'react-query'; import { useRouter } from 'next/router'; import EmptyPane from '@/components/empty-pane'; -import NamespaceMenu from '@/components/namespace-menu/namespace-menu'; import NewNamespace from '@/components/new-namespace'; import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; import { useGlobal } from '@/shared/services/global'; @@ -253,11 +252,7 @@ const NamespacesPage: React.FC = () => { my={0} > - + CAN'T SELECT NAMESPACE HERE ANYMORE! or Date: Fri, 3 May 2024 14:17:48 -0700 Subject: [PATCH 020/191] add query to whitelist --- .../httplocalhost4180managernamespaces-b2df18.gql | 1 + 1 file changed, 1 insertion(+) diff --git a/src/authz/graphql-whitelist/httplocalhost4180managernamespaces-b2df18.gql b/src/authz/graphql-whitelist/httplocalhost4180managernamespaces-b2df18.gql index ae6c0abe6..fa72e9122 100644 --- a/src/authz/graphql-whitelist/httplocalhost4180managernamespaces-b2df18.gql +++ b/src/authz/graphql-whitelist/httplocalhost4180managernamespaces-b2df18.gql @@ -3,6 +3,7 @@ allNamespaces { id name + displayName orgEnabled orgUpdatedAt } From 757e9054282c13036205175645ca8db05639befa Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Fri, 3 May 2024 09:01:07 -0700 Subject: [PATCH 021/191] remove old menu for no NSs --- .../pages/manager/namespaces/index.tsx | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index 352719e6a..9113bfe20 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -133,7 +133,6 @@ const NamespacesPage: React.FC = () => { { query }, { suspense: false } ); - console.log(data) const currentOrg = React.useMemo(() => { if (namespace.isSuccess && namespace.data.currentNamespace?.org) { return { @@ -266,30 +265,6 @@ const NamespacesPage: React.FC = () => { )} - {!hasNamespace && ( - <> - {/* this element needs to get moved or removed once logic is revised */} - - - - or - - - - - - )} {hasNamespace && ( From db43e06217635ebadc11357982ff1bcf2eecc0d3 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Thu, 30 May 2024 16:05:32 -0700 Subject: [PATCH 022/191] remove current gw from recently viewed --- .../components/namespace-menu/namespace-menu.tsx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index a1fdcfc20..b172ea1fc 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -48,9 +48,7 @@ const NamespaceMenu: React.FC = ({ const today = new Date(); const namespacesRecentlyViewed = JSON.parse(localStorage.getItem('namespacesRecentlyViewed') || '[]'); - const currentNamespace = data?.allNamespaces.find(namespace => namespace.name === user.namespace); - - let recentNamespaces = data?.allNamespaces + const recentNamespaces = data?.allNamespaces .filter((namespace: Namespace) => { const recentNamespace = namespacesRecentlyViewed.find((ns: any) => ns.namespace === namespace.name); return recentNamespace && recentNamespace.userId === user.userId && namespace.name !== user.namespace; @@ -60,13 +58,8 @@ const NamespaceMenu: React.FC = ({ const bRecent = namespacesRecentlyViewed.find((ns: any) => ns.namespace === b.name); return new Date(bRecent.updatedAt).getTime() - new Date(aRecent.updatedAt).getTime(); }) - .slice(0, 4); // Limit to 4, as we'll add the current namespace separately + .slice(0, 5); - // Add the current namespace to the beginning of the array - if (currentNamespace) { - recentNamespaces.unshift(currentNamespace); - } - const handleSearchChange = (value: string) => { setSearch(value); }; @@ -152,8 +145,8 @@ const NamespaceMenu: React.FC = ({ color="gray.600" box-shadow='0px 5px 15px 0px #38598A59' borderRadius={'10px'} - mt={-1} - pt={6} + mt={'-2px'} + py={6} sx={{ '.chakra-menu__group__title': { fontWeight: 'normal', From 346066ee3f734bcc181afed1e581a080dc9c1844 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Fri, 31 May 2024 14:33:39 -0700 Subject: [PATCH 023/191] edit display name - WIP, query and mutation inc --- ...plocalhost4180managernamespaces-339568.gql | 2 + .../edit-display-name/edit-display-name.tsx | 136 ++++++++++++++++++ .../components/edit-display-name/index.ts | 1 + .../pages/manager/namespaces/index.tsx | 6 +- .../shared/hooks/use-current-namespace.ts | 2 + 5 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/nextapp/components/edit-display-name/edit-display-name.tsx create mode 100644 src/nextapp/components/edit-display-name/index.ts diff --git a/src/authz/graphql-whitelist/httplocalhost4180managernamespaces-339568.gql b/src/authz/graphql-whitelist/httplocalhost4180managernamespaces-339568.gql index 02c8bfd69..3b70f716e 100644 --- a/src/authz/graphql-whitelist/httplocalhost4180managernamespaces-339568.gql +++ b/src/authz/graphql-whitelist/httplocalhost4180managernamespaces-339568.gql @@ -1,7 +1,9 @@ query GetCurrentNamespace { currentNamespace { + id name + displayName org orgUnit orgUpdatedAt diff --git a/src/nextapp/components/edit-display-name/edit-display-name.tsx b/src/nextapp/components/edit-display-name/edit-display-name.tsx new file mode 100644 index 000000000..2ff366bfb --- /dev/null +++ b/src/nextapp/components/edit-display-name/edit-display-name.tsx @@ -0,0 +1,136 @@ +import * as React from 'react'; +import { + Button, + ButtonGroup, + FormControl, + FormHelperText, + FormLabel, + Icon, + Input, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + useDisclosure, + useToast, +} from '@chakra-ui/react'; +import { FaPen } from 'react-icons/fa'; +import { QueryKey, useQueryClient } from 'react-query'; +import { useApiMutation } from '@/shared/services/api'; +import { gql } from 'graphql-request'; +import { NamespaceInput } from '@/shared/types/query.types'; + +interface EditNamespaceDisplayNameProps { + data: NamespaceInput; + queryKey: QueryKey; +} + +const EditNamespaceDisplayName: React.FC = ({ + data, + queryKey, +}) => { + const toast = useToast(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const queryClient = useQueryClient(); + const mutate = useApiMutation(mutation); + const form = React.useRef(); + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + updateNamespaceDisplayName(); + }; + const updateNamespaceDisplayName = async () => { + if (form.current) { + try { + const formData = new FormData(form.current); + + if (form.current.checkValidity()) { + const name = formData.get('name') as string; + const displayName = formData.get('displayName') as string; + await mutate.mutateAsync({ + // id: data?.id, + data: { name, displayName }, + }); + toast({ + title: 'Display name successfully edited', + status: 'success', + isClosable: true, + }); + queryClient.invalidateQueries(queryKey); + onClose(); + } + } catch (err) { + toast({ + title: 'Display name update failed', + description: err, + status: 'error', + isClosable: true, + }); + } + } + }; + const handleSaveClick = () => { + form.current?.requestSubmit(); + }; + return ( + <> + + + + + Edit display name + +
    + + + + A meaningful display name makes it easy for anyone to identify and distinguish this gateway from others. + + + +
    +
    + + + + + + +
    +
    + + ); +}; + +export default EditNamespaceDisplayName; + +const mutation = gql` + mutation UpdateNamespaceDisplayName($id: ID!, $data: NamespaceInput) { + updateNamespace(id: $id, data: $data) { + id + } + } +`; diff --git a/src/nextapp/components/edit-display-name/index.ts b/src/nextapp/components/edit-display-name/index.ts new file mode 100644 index 000000000..ab9c667a2 --- /dev/null +++ b/src/nextapp/components/edit-display-name/index.ts @@ -0,0 +1 @@ +export { default } from './edit-display-name'; diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index 9b99f17fa..747086a97 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -46,12 +46,14 @@ import { gql } from 'graphql-request'; import { restApi, useApiMutation } from '@/shared/services/api'; import { RiApps2Fill } from 'react-icons/ri'; import PreviewBanner from '@/components/preview-banner'; -import { useQueryClient } from 'react-query'; +import { QueryKey, useQueryClient } from 'react-query'; import { useRouter } from 'next/router'; import EmptyPane from '@/components/empty-pane'; +import { Namespace, Query } from '@/shared/types/query.types'; import NewNamespace from '@/components/new-namespace'; import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; import { useGlobal } from '@/shared/services/global'; +import EditNamespaceDisplayName from '@/components/edit-display-name'; const actions = [ { @@ -118,6 +120,7 @@ const NamespacesPage: React.FC = () => { const mutate = useApiMutation(mutation); const client = useQueryClient(); const namespace = useCurrentNamespace(); + const queryKey: QueryKey = ['allNamespaces']; const { isOpen, onClose, onOpen } = useDisclosure(); const global = useGlobal(); const currentOrg = React.useMemo(() => { @@ -167,6 +170,7 @@ const NamespacesPage: React.FC = () => { <> {user.namespace} + {namespace.data?.currentNamespace?.orgEnabled && ( { const query = gql` query GetCurrentNamespace { currentNamespace { + id name + displayName org orgUnit orgUpdatedAt From 1bdca869e729d3688be396cbeae85af843c23e42 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Mon, 3 Jun 2024 09:00:59 -0700 Subject: [PATCH 024/191] touch up ns search --- .../namespace-menu/namespace-menu.tsx | 17 +++++++++++++---- .../public/images/no_results_folder.png | Bin 0 -> 10322 bytes 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 src/nextapp/public/images/no_results_folder.png diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index b172ea1fc..4240c2b45 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -51,7 +51,7 @@ const NamespaceMenu: React.FC = ({ const recentNamespaces = data?.allNamespaces .filter((namespace: Namespace) => { const recentNamespace = namespacesRecentlyViewed.find((ns: any) => ns.namespace === namespace.name); - return recentNamespace && recentNamespace.userId === user.userId && namespace.name !== user.namespace; + return recentNamespace && namespace.name !== user.namespace; }) .sort((a, b) => { const aRecent = namespacesRecentlyViewed.find((ns: any) => ns.namespace === a.name); @@ -59,11 +59,9 @@ const NamespaceMenu: React.FC = ({ return new Date(bRecent.updatedAt).getTime() - new Date(aRecent.updatedAt).getTime(); }) .slice(0, 5); - const handleSearchChange = (value: string) => { setSearch(value); }; - const namespaceSearchResults = React.useMemo(() => { const result = data?.allNamespaces ?? []; @@ -155,7 +153,7 @@ const NamespaceMenu: React.FC = ({ }, }} > - + = ({ ) } > + {(search !== '' && namespaceSearchResults.length === 0) && ( + + Empty folder + + )} {(search !== '' ? namespaceSearchResults : recentNamespaces).map((n) => ( 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 aO=DBIv7(mY>Hxk2gjkqdFm5#TjQxL@5wSo3 literal 0 HcmV?d00001 From 56be56171ab24bd24db8d81d9daf7761d78d2e87 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Mon, 3 Jun 2024 09:03:42 -0700 Subject: [PATCH 025/191] style edit display name --- .../edit-display-name/edit-display-name.tsx | 64 +++++++++++++++---- .../namespace-menu/namespace-menu.tsx | 2 +- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/nextapp/components/edit-display-name/edit-display-name.tsx b/src/nextapp/components/edit-display-name/edit-display-name.tsx index 2ff366bfb..53578f406 100644 --- a/src/nextapp/components/edit-display-name/edit-display-name.tsx +++ b/src/nextapp/components/edit-display-name/edit-display-name.tsx @@ -13,6 +13,7 @@ import { ModalFooter, ModalHeader, ModalOverlay, + Text, useDisclosure, useToast, } from '@chakra-ui/react'; @@ -35,10 +36,29 @@ const EditNamespaceDisplayName: React.FC = ({ const { isOpen, onOpen, onClose } = useDisclosure(); const queryClient = useQueryClient(); const mutate = useApiMutation(mutation); + // console.log(data) + const [inputValue, setInputValue] = React.useState(data?.name || ''); + const [charCount, setCharCount] = React.useState(data?.name?.length || 0); + const charLimit = 30; + const handleInputChange = (event) => { + const { value } = event.target; + setInputValue(value); + setCharCount(value.length); + }; const form = React.useRef(); const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); - updateNamespaceDisplayName(); + if (charCount <= charLimit) { + // updateNamespaceDisplayName(); + submitTheForm(); + } + }; + const submitTheForm = async () => { + toast({ + title: 'Submitted it!', + status: 'success', + isClosable: true, + }) }; const updateNamespaceDisplayName = async () => { if (form.current) { @@ -84,38 +104,60 @@ const EditNamespaceDisplayName: React.FC = ({ > Edit - + - - Edit display name - + + Edit display name +
    - - A meaningful display name makes it easy for anyone to identify and distinguish this gateway from others. + + A meaningful display name makes it easy for anyone to identify and distinguish this Gateway from others. + charLimit ? 'bc-error' : 'gray.500'} + mt={2} + textAlign="right" + > + {charCount}/{charLimit} + charLimit} data-testid="edit-display-name-input" /> + {charCount > charLimit && ( + + You have reached the character limit + + )}
    - + - diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index 4240c2b45..15fb7e7e1 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -215,7 +215,7 @@ const NamespaceMenu: React.FC = ({ > - {n.displayName ? n.displayName : 'Display name here'} + {n.displayName ? n.displayName : `Gateway ${n.name}`} {n.name}
    From 1f549c5a86eb68172d760c06dbb2426682159d8f Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Mon, 3 Jun 2024 14:48:53 -0700 Subject: [PATCH 026/191] add displayName to mocks --- src/mocks/handlers.js | 2 ++ src/mocks/resolvers/namespace-access.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/mocks/handlers.js b/src/mocks/handlers.js index 40ec6ddc8..67fecb014 100644 --- a/src/mocks/handlers.js +++ b/src/mocks/handlers.js @@ -89,12 +89,14 @@ const allNamespaces = [ { id: 'n1', name: 'aps-portal', + displayName: 'API Services Portal gw', orgEnabled: true, createdAt: subDays(new Date(), 20).toISOString(), }, { id: 'n2', name: 'loc', + displayName: 'Location services', orgEnabled: false, createdAt: subDays(new Date(), 5).toISOString(), }, diff --git a/src/mocks/resolvers/namespace-access.js b/src/mocks/resolvers/namespace-access.js index b3828a257..f561fa06d 100644 --- a/src/mocks/resolvers/namespace-access.js +++ b/src/mocks/resolvers/namespace-access.js @@ -131,6 +131,7 @@ let umaPolicies = [ let currentNamespace = { id: 'ns1', name: 'aps-portal', + displayName: 'API Services Portal gw', scopes: [ { name: 'GatewayConfig.Publish' }, { name: 'Namespace.Manage' }, @@ -172,6 +173,7 @@ export const updateCurrentNamesSpaceHandler = (req, res, ctx) => { ...req.variables, org: req.variables.org ? { title: req.variables.org } : null, orgUnit: req.variables.orgUnit ? { title: req.variables.orgUnit } : null, + displayName: req.variables.displayName ? { title: req.variables.displayName } : null, }; return res(ctx.data({})); }; From 5dde66ff978bccd4ecd80fcdbd37e379258152d2 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Mon, 3 Jun 2024 14:51:24 -0700 Subject: [PATCH 027/191] show ns displayname on namespaces page --- .../edit-display-name/edit-display-name.tsx | 41 ++---- .../pages/manager/namespaces/index.tsx | 126 +++++++++--------- 2 files changed, 77 insertions(+), 90 deletions(-) diff --git a/src/nextapp/components/edit-display-name/edit-display-name.tsx b/src/nextapp/components/edit-display-name/edit-display-name.tsx index c68d2cb89..3772748af 100644 --- a/src/nextapp/components/edit-display-name/edit-display-name.tsx +++ b/src/nextapp/components/edit-display-name/edit-display-name.tsx @@ -35,10 +35,9 @@ const EditNamespaceDisplayName: React.FC = ({ const toast = useToast(); const { isOpen, onOpen, onClose } = useDisclosure(); const queryClient = useQueryClient(); - const mutate = useApiMutation(mutation); - // console.log(data) - const [inputValue, setInputValue] = React.useState(data?.name || ''); - const [charCount, setCharCount] = React.useState(data?.name?.length || 0); + const mutate = useApiMutation(mutation); + const [inputValue, setInputValue] = React.useState(data.displayName || ''); + const [charCount, setCharCount] = React.useState(data.displayName?.length || 0); const charLimit = 30; const handleInputChange = (event) => { const { value } = event.target; @@ -49,36 +48,23 @@ const EditNamespaceDisplayName: React.FC = ({ const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); if (charCount <= charLimit) { - // updateNamespaceDisplayName(); - submitTheForm(); + updateNamespaceDisplayName(); } }; - const submitTheForm = async () => { - toast({ - title: 'Submitted it!', - status: 'success', - isClosable: true, - }) - }; const updateNamespaceDisplayName = async () => { if (form.current) { try { - const formData = new FormData(form.current); - if (form.current.checkValidity()) { - const name = formData.get('name') as string; - const displayName = formData.get('displayName') as string; - await mutate.mutateAsync({ - // id: data?.id, - data: { name, displayName }, - }); + const formData = new FormData(form.current); + const entries = Object.fromEntries(formData); + await mutate.mutateAsync(entries); + queryClient.invalidateQueries(queryKey); + onClose(); toast({ title: 'Display name successfully edited', status: 'success', isClosable: true, }); - queryClient.invalidateQueries(queryKey); - onClose(); } } catch (err) { toast({ @@ -96,6 +82,7 @@ const EditNamespaceDisplayName: React.FC = ({ return ( <>
    - - {data && ( - - )} ); }; From 07c68bf8495905e466b7dc9077cf8d8512a2ed5c Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Tue, 4 Jun 2024 13:56:13 -0700 Subject: [PATCH 032/191] make full gateways list link a MenuItem --- .../namespace-menu/namespace-menu.tsx | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index dfd466910..05be49ce3 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -34,8 +34,6 @@ const NamespaceMenu: React.FC = ({ const client = useQueryClient(); const toast = useToast(); const [search, setSearch] = React.useState(''); - const searchInputRef = React.useRef(null); - const managerDisclosure = useDisclosure(); const { data, isLoading, isSuccess, isError, refetch } = useApi( 'allNamespaces', { query }, @@ -48,12 +46,7 @@ const NamespaceMenu: React.FC = ({ setSearch(value); handleRefresh(); }; - // useEffect(() => { - // if (isOpen.isOpen && searchInputRef.current) { - // searchInputRef.current.focus(); - // } - // }, [isOpen.isOpen, searchInputRef]); - + const namespacesRecentlyViewed = JSON.parse(localStorage.getItem('namespacesRecentlyViewed') || '[]'); const recentNamespaces = data?.allNamespaces .filter((namespace: Namespace) => { @@ -148,7 +141,8 @@ const NamespaceMenu: React.FC = ({ box-shadow='0px 5px 15px 0px #38598A59' borderRadius={'10px'} mt={'-2px'} - py={6} + pt={6} + pb={4} sx={{ '.chakra-menu__group__title': { fontWeight: 'normal', @@ -232,9 +226,23 @@ const NamespaceMenu: React.FC = ({ data.allNamespaces.length !== 1 ? 's' : '' } in total`} - - Go to the full Gateways list - + + Go to the{' '} + + full Gateways list + +
    )} From 6ffafd1d7126387e7962b93dbb9d7ddd8f80d567 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Tue, 4 Jun 2024 14:00:49 -0700 Subject: [PATCH 033/191] cleanup --- src/nextapp/pages/manager/namespaces/index.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index ba8f48674..20cc9cbac 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -50,7 +50,6 @@ import { QueryKey, useQueryClient } from 'react-query'; import { useRouter } from 'next/router'; import EmptyPane from '@/components/empty-pane'; import { Namespace, Query } from '@/shared/types/query.types'; -import NewNamespace from '@/components/new-namespace'; import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; import { useGlobal } from '@/shared/services/global'; import EditNamespaceDisplayName from '@/components/edit-display-name'; @@ -270,13 +269,8 @@ const NamespacesPage: React.FC = () => { my={0} > - CAN'T SELECT NAMESPACE HERE ANYMORE! - or - + This page will be replaced. -
    )} {hasNamespace && ( From 8e71187612681b6148d2549f6f0e52cab07e42a8 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Tue, 4 Jun 2024 14:05:36 -0700 Subject: [PATCH 034/191] update Your Products page header --- src/nextapp/pages/devportal/api-directory/your-products.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/nextapp/pages/devportal/api-directory/your-products.tsx b/src/nextapp/pages/devportal/api-directory/your-products.tsx index 4f2c0033f..ee5954f55 100644 --- a/src/nextapp/pages/devportal/api-directory/your-products.tsx +++ b/src/nextapp/pages/devportal/api-directory/your-products.tsx @@ -9,6 +9,7 @@ import { useQuery } from 'react-query'; import { Dataset, Product } from '@/shared/types/query.types'; import PreviewBanner from '@/components/preview-banner'; import DiscoveryList from '@/components/discovery-list'; +import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; import { useAuth } from '@/shared/services/auth'; import NextLink from 'next/link'; @@ -23,6 +24,7 @@ const ApiDiscoveryPage: React.FC = () => { `/ds/api/v2/namespaces/${user?.namespace}/directory` ) ); + const namespace = useCurrentNamespace(); return ( <> @@ -33,7 +35,7 @@ const ApiDiscoveryPage: React.FC = () => { {user && From f3abe752f8968dc2c9d3e1d0970ece81434d86dc Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Tue, 4 Jun 2024 14:16:45 -0700 Subject: [PATCH 035/191] refresh namespaces when opening menu --- src/nextapp/components/namespace-menu/namespace-menu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index 05be49ce3..0171cedf5 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -113,7 +113,7 @@ const NamespaceMenu: React.FC = ({ return ( <> - + Date: Tue, 4 Jun 2024 14:36:57 -0700 Subject: [PATCH 036/191] first draft --- local/oauth2-proxy/oauth2-proxy-dev.cfg | 27 + src/api-openapi.js | 34 +- src/auth/auth-tsoa.ts | 2 + src/batch/data-rules.js | 12 + src/controllers/v2/types.ts | 10 + src/controllers/v3/DatasetController.ts | 105 ++ src/controllers/v3/DirectoryController.ts | 182 ++ src/controllers/v3/GatewayController.ts | 284 +++ .../v3/GatewayDirectoryController.ts | 178 ++ .../v3/GatewayServicesController.ts | 84 + src/controllers/v3/IdentifierController.ts | 26 + src/controllers/v3/IssuerController.ts | 128 ++ src/controllers/v3/OrgDatasetController.ts | 208 +++ src/controllers/v3/OrgRoleController.ts | 12 + src/controllers/v3/OrganizationController.ts | 272 +++ src/controllers/v3/ProductController.ts | 217 +++ src/controllers/v3/openapi.yaml | 1562 ++++++++++++++++ src/controllers/v3/routes.ts | 1591 +++++++++++++++++ src/controllers/v3/types-extra.ts | 21 + src/controllers/v3/types.ts | 574 ++++++ src/package.json | 5 +- src/tools/tsoaTypes.js | 227 +-- src/tsoa-v3.json | 73 + 23 files changed, 5725 insertions(+), 109 deletions(-) create mode 100644 local/oauth2-proxy/oauth2-proxy-dev.cfg create mode 100644 src/controllers/v3/DatasetController.ts create mode 100644 src/controllers/v3/DirectoryController.ts create mode 100644 src/controllers/v3/GatewayController.ts create mode 100644 src/controllers/v3/GatewayDirectoryController.ts create mode 100644 src/controllers/v3/GatewayServicesController.ts create mode 100644 src/controllers/v3/IdentifierController.ts create mode 100644 src/controllers/v3/IssuerController.ts create mode 100644 src/controllers/v3/OrgDatasetController.ts create mode 100644 src/controllers/v3/OrgRoleController.ts create mode 100644 src/controllers/v3/OrganizationController.ts create mode 100644 src/controllers/v3/ProductController.ts create mode 100644 src/controllers/v3/openapi.yaml create mode 100644 src/controllers/v3/routes.ts create mode 100644 src/controllers/v3/types-extra.ts create mode 100644 src/controllers/v3/types.ts create mode 100644 src/tsoa-v3.json diff --git a/local/oauth2-proxy/oauth2-proxy-dev.cfg b/local/oauth2-proxy/oauth2-proxy-dev.cfg new file mode 100644 index 000000000..d616f19c1 --- /dev/null +++ b/local/oauth2-proxy/oauth2-proxy-dev.cfg @@ -0,0 +1,27 @@ +http_address="0.0.0.0:4180" +cookie_secret="abcd1234!@#$$++=" +email_domains="*" +provider="oidc" +insecure_oidc_allow_unverified_email="true" +client_id="aps-portal" +client_secret="8e1a17ed-cb93-4806-ac32-e303d1c86018" +scope="openid" +oidc_issuer_url="http://keycloak.localtest.me:9081/auth/realms/master" +login_url="http://keycloak.localtest.me:9081/auth/realms/master/protocol/openid-connect/auth" +redeem_url="http://keycloak.localtest.me:9081/auth/realms/master/protocol/openid-connect/token" +validate_url="http://keycloak.localtest.me:9081/auth/realms/master/protocol/openid-connect/userinfo" +redirect_url="http://oauth2proxy.localtest.me:4180/oauth2/callback" +profile_url="http://keycloak.localtest.me:9081/auth/realms/master/protocol/openid-connect/userinfo" +cookie_secure="false" +cookie_refresh="3m" +cookie_expire="24h" +pass_basic_auth="false" +pass_access_token="true" +set_xauthrequest="true" +skip_jwt_bearer_tokens="false" +set_authorization_header="false" +pass_authorization_header="false" +skip_auth_regex="/__coverage__|/login|/health|/public|/docs|/redirect|/_next|/images|/devportal|/manager|/about|/maintenance|/admin/session|/ds/api|/gw/api|/feed/|/signout|^[/]$" +whitelist_domains="keycloak.localtest.me:9081" +upstreams=["http://192.168.1.69:3000"] +skip_provider_button='true' diff --git a/src/api-openapi.js b/src/api-openapi.js index c84c07e31..115ba0274 100644 --- a/src/api-openapi.js +++ b/src/api-openapi.js @@ -31,7 +31,7 @@ class ApiOpenapiApp { } prepareV2(app) { - const { RegisterRoutes } = require('./controllers/v2/routes'); + const { RegisterRoutes: V2Register } = require('./controllers/v2/routes'); const specFile = fs.realpathSync('controllers/v2/openapi.yaml'); const specObject = YAML.load(fs.readFileSync(specFile)); @@ -50,7 +50,7 @@ class ApiOpenapiApp { specObject.components.securitySchemes.openid.openIdConnectUrl = `${process.env.OIDC_ISSUER}/.well-known/openid-configuration`; - RegisterRoutes(app); + V2Register(app); app.get('/ds/api/v2/openapi.yaml', (req, res) => { res.setHeader('Content-Type', 'application/yaml'); @@ -59,8 +59,31 @@ class ApiOpenapiApp { app.use( '/ds/api/v2/console', - swaggerUi.serve, - swaggerUi.setup(specObject, options) + swaggerUi.serveFiles(specObject, options), + swaggerUi.setup(specObject) + ); + } + + prepareV3(app) { + const { RegisterRoutes: V3Register } = require('./controllers/v3/routes'); + const specFile = fs.realpathSync('controllers/v3/openapi.yaml'); + const specObject = YAML.load(fs.readFileSync(specFile)); + + specObject.components.securitySchemes.jwt.flows.clientCredentials.tokenUrl = `${process.env.OIDC_ISSUER}/protocol/openid-connect/token`; + + specObject.components.securitySchemes.openid.openIdConnectUrl = `${process.env.OIDC_ISSUER}/.well-known/openid-configuration`; + + V3Register(app); + + app.get('/ds/api/v3/openapi.yaml', (req, res) => { + res.setHeader('Content-Type', 'application/yaml'); + res.send(YAML.dump(specObject)); + }); + + app.use( + '/ds/api/v3/console', + swaggerUi.serveFiles(specObject, options), + swaggerUi.setup(specObject) ); } @@ -72,13 +95,14 @@ class ApiOpenapiApp { Register(keystone); + this.prepareV3(app); this.prepareV2(app); this.prepareV1(app); // RFC 8631 service-desc link relation // https://datatracker.ietf.org/doc/html/rfc8631 app.get('/ds/api', (req, res) => { - res.setHeader('Link', '; rel="service-desc"'); + res.setHeader('Link', '; rel="service-desc"'); res.status(204).end(); }); diff --git a/src/auth/auth-tsoa.ts b/src/auth/auth-tsoa.ts index 97ef15c86..7f9decf70 100644 --- a/src/auth/auth-tsoa.ts +++ b/src/auth/auth-tsoa.ts @@ -55,6 +55,8 @@ export function expressAuthentication( resource = `org/${request.params.orgUnit}`; } else if ('org' in request.params) { resource = `org/${request.params.org}`; + } else if ('gatewayId' in request.params) { + resource = request.params.gatewayId; } else { // assume it is namespace-based protection resource = request.params.ns; diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index 1d243a22b..40f87b964 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -207,6 +207,18 @@ const metadata = { // }, }, }, + Gateway: { + query: 'allNamespaces', + refKey: 'gatewayId', + sync: ['displayName'], + transformations: { + // members: { + // name: 'connectExclusiveList', + // list: 'MemberRole', + // syncFirst: true, + // }, + }, + }, MemberRole: { query: 'allMemberRoles', refKey: 'extRefId', diff --git a/src/controllers/v2/types.ts b/src/controllers/v2/types.ts index a5ed8e61e..f610a40cb 100644 --- a/src/controllers/v2/types.ts +++ b/src/controllers/v2/types.ts @@ -138,6 +138,16 @@ export interface Namespace { } +/** + * @tsoaModel + * + */ +export interface Gateway { + gatewayId?: string; // Primary Key + displayName?: string; +} + + /** * @tsoaModel * diff --git a/src/controllers/v3/DatasetController.ts b/src/controllers/v3/DatasetController.ts new file mode 100644 index 000000000..c9434d52c --- /dev/null +++ b/src/controllers/v3/DatasetController.ts @@ -0,0 +1,105 @@ +import { + Body, + Controller, + OperationId, + Request, + Put, + Path, + Route, + Security, + Tags, + Get, +} from 'tsoa'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; +import { + getRecord, + parseJsonString, + removeEmpty, + removeKeys, + syncRecordsThrowErrors, + transformAllRefID, +} from '../../batch/feed-worker'; +import { Dataset, DraftDataset } from './types'; +import { transformContacts, transformResources } from './OrgDatasetController'; +import { BatchResult } from '../../batch/types'; +import { transform } from './DirectoryController'; +import { gql } from 'graphql-request'; +import { Product } from '@/services/keystone/types'; + +@injectable() +@Route('/gateways/{gatewayId}/datasets') +@Security('jwt', ['Namespace.Manage']) +@Tags('API Directory') +export class DatasetController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + /** + * Update metadata about a Dataset + * > `Required Scope:` Namespace.Manage + * + * @summary Update Dataset + */ + @Put() + @OperationId('put-dataset') + public async put( + @Path() gatewayId: string, + @Body() body: DraftDataset, + @Request() request: any + ): Promise { + // rules: + // - isInDraft can not be changed (only by Organization) + // - isInCatalog must be false (OrgDataset should only be updating this) + removeKeys(body, ['isInDraft', 'isInCatalog']); + + return await syncRecordsThrowErrors( + this.keystone.createContext(request), + 'DraftDataset', + request.body['name'], + request.body + ); + } + + /** + * Get metadata about a Dataset + * > `Required Scope:` Namespace.Manage + * + * @summary Get Dataset + */ + @Get('{name}') + @OperationId('get-dataset') + public async getDataset( + @Path() gatewayId: string, + @Path() name: string, + @Request() request: any + ): Promise { + const ctx = this.keystone.createContext(request); + + const record = await getRecord(ctx, 'DraftDataset', name, [ + 'organization', + 'organizationUnit', + ]); + + return [record] + .map((o) => removeEmpty(o)) + .map((o) => transformAllRefID(o, [])) + .map((o) => parseJsonString(o, ['tags', 'contacts', 'resources'])) + .map((o) => + removeKeys(o, [ + 'id', + 'namespace', + 'extSource', + 'extForeignKey', + 'extRecordHash', + 'orgUnits', + ]) + ) + .map((o) => transformContacts(o)) + .map((o) => transformResources(o)) + .pop(); + } +} diff --git a/src/controllers/v3/DirectoryController.ts b/src/controllers/v3/DirectoryController.ts new file mode 100644 index 000000000..1d6cf5e53 --- /dev/null +++ b/src/controllers/v3/DirectoryController.ts @@ -0,0 +1,182 @@ +import { Controller, OperationId, Get, Path, Route, Tags } from 'tsoa'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; +import { gql } from 'graphql-request'; +import { Product } from '../../services/keystone/types'; +import { + removeEmpty, + removeKeys, + parseJsonString, + transformAllRefID, +} from '../../batch/feed-worker'; + +@injectable() +@Route('/directory') +@Tags('API Directory') +export class DirectoryController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + @Get() + @OperationId('directory-list') + public async list(): Promise { + const result = await this.keystone.executeGraphQL({ + context: this.keystone.sudo(), + query: list, + }); + return transform(result.data.allDiscoverableProducts); + } + + @Get('{id}') + @OperationId('directory-item') + public async get(@Path() id: string): Promise { + const result = await this.keystone.executeGraphQL({ + context: this.keystone.sudo(), + query: item, + variables: { id }, + }); + if (result.data.allDiscoverableProducts.length == 0) { + return null; + } + + return transform( + transformSetAnonymous(result.data.allDiscoverableProducts) + )[0]; + } +} + +export function transformSetAnonymous(products: Product[]) { + products.forEach((prod) => { + prod.environments.forEach((env) => { + env.services.forEach((svc) => { + function setAnonymousIfApplicable(plugins: any[]) { + plugins + ?.filter( + (plugin) => + plugin.name == 'key-auth' || plugin.name == 'jwt-keycloak' + ) + .forEach((plugin) => { + const config = JSON.parse(plugin.config); + if (config.anonymous) { + (env as any).anonymous = true; + } + }); + } + setAnonymousIfApplicable(svc.plugins); + svc.routes?.forEach((route) => { + setAnonymousIfApplicable(route.plugins); + }); + }); + }); + }); + return products; +} + +export function transform(products: Product[]) { + const records: Product[] = products.reduce((accumulator: any, prod: any) => { + if (prod.dataset === null) { + // drop it + } else { + const dataset = accumulator.filter( + (a: any) => a.name === prod.dataset?.name + ); + if (dataset.length == 0) { + accumulator.push(prod.dataset); + prod.dataset.products = [{ ...prod, dataset: null }]; + } else { + dataset[0].products.push({ ...prod, dataset: null }); + } + } + return accumulator; + }, []); + + return records + .map((o) => removeEmpty(o)) + .map((o) => parseJsonString(o, ['tags'])); +} + +const list = gql` + query Directory { + allDiscoverableProducts(where: { environments_some: { active: true } }) { + id + name + environments { + name + active + flow + } + dataset { + id + name + title + notes + license_title + view_audience + security_class + record_publish_date + tags + organization { + name + title + } + organizationUnit { + name + title + } + } + } + } +`; + +const item = gql` + query GetProduct($id: ID!) { + allDiscoverableProducts( + where: { environments_some: { active: true }, dataset: { id: $id } } + ) { + id + name + environments { + name + active + flow + services { + name + host + plugins { + name + tags + config + } + routes { + plugins { + name + config + } + } + } + } + dataset { + name + title + notes + license_title + security_class + view_audience + tags + record_publish_date + isInCatalog + organization { + name + title + } + organizationUnit { + name + title + } + } + } + } +`; diff --git a/src/controllers/v3/GatewayController.ts b/src/controllers/v3/GatewayController.ts new file mode 100644 index 000000000..d44802a2d --- /dev/null +++ b/src/controllers/v3/GatewayController.ts @@ -0,0 +1,284 @@ +import { + Controller, + OperationId, + Request, + Get, + Path, + Route, + Security, + Tags, + Delete, + Query, + Post, + Body, +} from 'tsoa'; +import { ValidateError, FieldErrors } from 'tsoa'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; +import { gql } from 'graphql-request'; +import { WorkbookService } from '../../services/report/workbook.service'; +import { Namespace, NamespaceInput } from '../../services/keystone/types'; + +import { Readable } from 'stream'; +import { + parseBlobString, + parseJsonString, + removeEmpty, + removeKeys, + transformAllRefID, +} from '../../batch/feed-worker'; + +import { strict as assert } from 'assert'; + +import { Logger } from '../../logger'; +import { Activity, Gateway } from './types'; +import { getActivity } from '../../services/keystone/activity'; +import { transformActivity } from '../../services/workflow'; +import { ActivityDetail } from './types-extra'; + +const logger = Logger('controllers.Namespace'); + +/** + * @param binary Buffer + * returns readableInstanceStream Readable + */ +function bufferToStream(binary: any) { + const readableInstanceStream = new Readable({ + read() { + this.push(binary); + this.push(null); + }, + }); + + return readableInstanceStream; +} + +@injectable() +@Route('/gateways') +@Security('jwt') +@Tags('Gateways') +export class NamespaceController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + @Get('/report') + @OperationId('report') + public async report( + @Request() req: any, + @Query() ids: string = '[]' + ): Promise { + const workbookService = new WorkbookService( + this.keystone.createContext(req, true) + ); + const workbook = await workbookService.buildWorkbook(JSON.parse(ids)); + const buffer = await workbook.xlsx.writeBuffer(); + + req.res.setHeader( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ); + req.res.setHeader( + 'Content-Disposition', + 'attachment; filename="bcgov_app_gateways.xlsx"' + ); + + const mystream = bufferToStream(buffer); + mystream.pipe(req.res); + await new Promise((resolve, reject) => { + mystream.on('end', () => { + req.res.end(); + resolve(null); + }); + }); + + return null; + } + + /** + * @summary List of Gateway IDs + * @param request + * @returns + */ + @Get() + @OperationId('gateway-list') + public async list(@Request() request: any): Promise { + const result = await this.keystone.executeGraphQL({ + context: this.keystone.createContext(request), + query: list, + }); + logger.debug('Result %j', result); + return result.data.allNamespaces.map((ns: Namespace) => ns.name).sort(); + } + + /** + * Get details about the gateway, such as permissions for what the gateway is setup with. + * > `Required Scope:` Namespace.Manage + * + * @summary Gateway Summary + * @param ns + * @param request + * @returns + */ + @Get('/{gatewayId}') + @OperationId('namespace-profile') + @Security('jwt', ['Namespace.Manage']) + public async profile( + @Path() gatewayId: string, + @Request() request: any + ): Promise { + const result = await this.keystone.executeGraphQL({ + context: this.keystone.createContext(request), + query: item, + variables: { ns: gatewayId }, + }); + logger.debug('Result %j', result); + assert.strictEqual('errors' in result, false, 'Unable to process request'); + return result.data.namespace; + } + + /** + * Create a gateway + * + * @summary Create Gateway + * @param ns + * @param request + * @returns + */ + @Post() + @OperationId('create-gateway') + @Security('jwt', []) + public async create( + @Request() request: any, + @Body() vars: NamespaceInput + ): Promise { + const result = await this.keystone.executeGraphQL({ + context: this.keystone.createContext(request), + query: createNS, + variables: vars, + }); + logger.debug('Result %j', result); + if (result.errors) { + const errors: FieldErrors = {}; + result.errors.forEach((err: any, ind: number) => { + errors[`d${ind}`] = { message: err.message }; + }); + throw new ValidateError(errors, 'Unable to create namespace'); + } + return { + gatewayId: result.data.createNamespace.name, + displayName: result.data.createNamespace.displayName, + }; + } + + /** + * Delete the gateway + * > `Required Scope:` Namespace.Manage + * + * @summary Delete Gateway + * @param ns + * @param request + * @returns + */ + @Delete('/{gatewayId}') + @OperationId('delete-namespace') + @Security('jwt', ['Namespace.Manage']) + public async delete( + @Path() gatewayId: string, + @Query() force: boolean = false, + @Request() request: any + ): Promise { + const ns = gatewayId; + const result = await this.keystone.executeGraphQL({ + context: this.keystone.createContext(request), + query: deleteNS, + variables: { ns, force }, + }); + logger.debug('Result %j', result); + if (result.errors) { + const errors: FieldErrors = {}; + result.errors.forEach((err: any, ind: number) => { + errors[`d${ind}`] = { message: err.message }; + }); + throw new ValidateError(errors, 'Unable to delete gateway'); + } + return result.data.forceDeleteNamespace; + } + + /** + * > `Required Scope:` Namespace.View + * + * @summary Get administration activity for this gateway + * @param ns + * @param first + * @param skip + * @returns Activity[] + */ + @Get('/{gatewayId}/activity') + @OperationId('gateway-admin-activity') + @Security('jwt', ['Namespace.View']) + public async namespaceActivity( + @Path() gatewayId: string, + @Query() first: number = 20, + @Query() skip: number = 0 + ): Promise { + const ctx = this.keystone.sudo(); + const records = await getActivity( + ctx, + [gatewayId], + undefined, + first > 100 ? 100 : first, + skip + ); + return transformActivity(records) + .map((o) => removeKeys(o, ['id'])) + .map((o) => removeEmpty(o)) + .map((o) => parseJsonString(o, ['context'])) + .map((o) => parseBlobString(o)); + } +} + +const list = gql` + query Namespaces { + allNamespaces { + name + } + } +`; + +const item = gql` + query Namespace($gatewayId: String!) { + namespace(gatewayId: $ns) { + name + displayName + scopes { + name + } + permDomains + permDataPlane + permProtectedNs + org + orgUnit + orgUpdatedAt + orgEnabled + orgAdmins + } + } +`; + +const deleteNS = gql` + mutation ForceDeleteNamespace($gatewayId: String!, $force: Boolean!) { + forceDeleteNamespace(namespace: $ns, force: $force) + } +`; + +const createNS = gql` + mutation CreateNamespace($name: String, $displayName: String) { + createNamespace(name: $name, displayName: $displayName) { + name + displayName + } + } +`; diff --git a/src/controllers/v3/GatewayDirectoryController.ts b/src/controllers/v3/GatewayDirectoryController.ts new file mode 100644 index 000000000..48c14b9d3 --- /dev/null +++ b/src/controllers/v3/GatewayDirectoryController.ts @@ -0,0 +1,178 @@ +import { + Controller, + OperationId, + Request, + Path, + Route, + Security, + Tags, + Get, +} from 'tsoa'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; +import { transform, transformSetAnonymous } from './DirectoryController'; +import { gql } from 'graphql-request'; +import { strict as assert } from 'assert'; + +@injectable() +@Route('/gateways/{gatewayId}/directory') +@Security('jwt', ['Namespace.Manage']) +@Tags('API Directory') +export class NamespaceDirectoryController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + /** + * Used primarily for "Preview Mode" + * Get a particular Dataset + * + * @param ns + * @param name + * @param request + * @returns + */ + @Get('{id}') + @OperationId('get-ns-directory-dataset') + public async getDataset( + @Path() gatewayId: string, + @Path() id: string, + @Request() request: any + ): Promise { + const context = this.keystone.createContext(request); + const result = await this.keystone.executeGraphQL({ + context, + query: item, + variables: { id }, + }); + + assert.strictEqual( + result.data.allProductsByNamespace?.length == 0, + false, + 'No products for dataset found' + ); + + return transform( + transformSetAnonymous(result.data.allProductsByNamespace) + )[0]; + } + + /** + * Used primarily for "Preview Mode" + * List the datasets belonging to a particular namespace + * + * @param ns + * @param name + * @param request + * @returns + */ + @Get() + @OperationId('get-ns-directory') + public async getDatasets( + @Path() gatewayId: string, + @Request() request: any + ): Promise { + const context = this.keystone.createContext(request); + const result = await this.keystone.executeGraphQL({ + context, + query: list, + }); + // For Preview, put a placeholder Dataset so that it gets returned + // result.data.allProductsByNamespace + // .filter((prod: Product) => !prod.dataset) + // .forEach((prod: Product) => { + // prod.dataset = { + // id: '--', + // name: 'Placeholder Dataset', + // title: 'Placeholder Dataset', + // isInCatalog: false, + // isDraft: true, + // }; + // }); + + return transform(result.data.allProductsByNamespace); + } +} + +const list = gql` + query DirectoryNamespacePreview { + allProductsByNamespace { + id + name + environments { + name + active + flow + } + dataset { + id + name + title + notes + license_title + view_audience + security_class + record_publish_date + tags + organization { + name + title + } + organizationUnit { + name + title + } + } + } + } +`; + +const item = gql` + query DirectoryNamespaceDataset($id: ID!) { + allProductsByNamespace(where: { dataset: { id: $id } }) { + id + name + environments { + name + active + flow + services { + name + host + plugins { + name + tags + config + } + routes { + plugins { + name + config + } + } + } + } + dataset { + name + title + notes + license_title + security_class + view_audience + tags + record_publish_date + isInCatalog + organization { + name + title + } + organizationUnit { + name + title + } + } + } + } +`; diff --git a/src/controllers/v3/GatewayServicesController.ts b/src/controllers/v3/GatewayServicesController.ts new file mode 100644 index 000000000..927087567 --- /dev/null +++ b/src/controllers/v3/GatewayServicesController.ts @@ -0,0 +1,84 @@ +import { + Controller, + Request, + OperationId, + Get, + Put, + Path, + Route, + Security, + Body, + Tags, + FormField, + UploadedFile, +} from 'tsoa'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; +import { + syncRecords, + getRecords, + parseJsonString, + removeEmpty, + removeKeys, +} from '../../batch/feed-worker'; +import { GatewayRoute } from './types'; +import { PublishResult } from './types-extra'; + +@injectable() +@Route('/gateways/{gatewayId}/services') +@Tags('Gateway Services') +export class GatewayController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + @Put() + @OperationId('publish-gateway-config') + @Security('jwt', ['Gateway.Config']) + public async put( + @FormField() dryRun: boolean, + @UploadedFile() configFile: Express.Multer.File + ): Promise { + // stub - gwa-api implements this + return { error: 'Stub - not implemented' }; + } + + /** + * Get a summary of your Gateway Services + * > `Required Scope:` Namespace.Manage + * + * @summary Get Gateway Services + */ + @Get() + @OperationId('get-gateway-routes') + @Security('jwt', ['Namespace.Manage']) + public async get( + @Path() gatewayId: string, + @Request() request: any + ): Promise { + const ctx = this.keystone.createContext(request); + const records = await getRecords( + ctx, + 'GatewayRoute', + 'allGatewayRoutesByNamespace', + ['plugins', 'service'] + ); + + return records + .map((o) => removeEmpty(o)) + .map((o) => + parseJsonString(o, ['tags', 'config', 'paths', 'hosts', 'methods']) + ) + .map((o) => + removeKeys(o, [ + 'id', + 'namespace', + 'extSource', + 'extRecordHash', + 'extForeignKey', + ]) + ); + } +} diff --git a/src/controllers/v3/IdentifierController.ts b/src/controllers/v3/IdentifierController.ts new file mode 100644 index 000000000..b37fc2657 --- /dev/null +++ b/src/controllers/v3/IdentifierController.ts @@ -0,0 +1,26 @@ +import { Controller, Get, Path, Route, Tags } from 'tsoa'; + +import { + newProductID, + newEnvironmentID, + newApplicationID, +} from '../../services/identifiers'; + +@Route('identifiers') +@Tags('New Identifiers') +export class IdentifiersController extends Controller { + @Get('{type}') + public async getNewID( + @Path() type: 'environment' | 'product' | 'application' + ): Promise { + if (type == 'environment') { + return newEnvironmentID(); + } else if (type == 'product') { + return newProductID(); + } else if (type == 'application') { + return newApplicationID(); + } else { + return ''; + } + } +} diff --git a/src/controllers/v3/IssuerController.ts b/src/controllers/v3/IssuerController.ts new file mode 100644 index 000000000..98598932c --- /dev/null +++ b/src/controllers/v3/IssuerController.ts @@ -0,0 +1,128 @@ +import { + Controller, + Request, + Delete, + Get, + OperationId, + Put, + Path, + Route, + Security, + Body, + Tags, +} from 'tsoa'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; +import { + syncRecordsThrowErrors, + getRecords, + removeEmpty, + removeKeys, + parseJsonString, + transformAllRefID, + getRecord, + deleteRecord, +} from '../../batch/feed-worker'; +import { CredentialIssuer } from './types'; +import { BatchResult } from '../../batch/types'; +import { strict as assert } from 'assert'; + +@injectable() +@Route('/gateways/{gatewayId}/issuers') +@Tags('Authorization Profiles') +export class IssuerController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + /** + * Create or Update Authorization Profiles + * > `Required Scope:` CredentialIssuer.Admin + * + * @summary Manage Authorization Profiles + */ + @Put() + @OperationId('put-issuer') + @Security('jwt', ['CredentialIssuer.Admin']) + public async put( + @Path() gatewayId: string, + @Body() body: CredentialIssuer, + @Request() request: any + ): Promise { + return await syncRecordsThrowErrors( + this.keystone.createContext(request), + 'CredentialIssuer', + body['name'], + body + ); + } + + /** + * Get Authorization Profiles setup in this namespace + * > `Required Scope:` Namespace.Manage + * + * @summary Get Authorization Profiles + */ + @Get() + @OperationId('get-issuers') + @Security('jwt', ['Namespace.Manage']) + public async get( + @Path() gatewayId: string, + @Request() request: any + ): Promise { + const ctx = this.keystone.createContext(request); + const records = await getRecords( + ctx, + 'CredentialIssuer', + 'allCredentialIssuersByNamespace', + [] + ); + + return records + .map((o) => removeEmpty(o)) + .map((o) => transformAllRefID(o, ['owner'])) + .map((o) => + parseJsonString(o, [ + 'availableScopes', + 'resourceScopes', + 'clientRoles', + 'clientMappers', + 'environmentDetails', + ]) + ) + .map((o) => + removeKeys(o, [ + 'id', + 'namespace', + 'extSource', + 'extForeignKey', + 'extRecordHash', + ]) + ); + } + + /** + * Delete an Authorization Profile + * > `Required Scope:` CredentialIssuer.Admin + * + * @summary Delete Profile + */ + @Delete('/{name}') + @OperationId('delete-issuer') + @Security('jwt', ['CredentialIssuer.Admin']) + public async delete( + @Path() gatewayId: string, + @Path() name: string, + @Request() request: any + ): Promise { + const context = this.keystone.createContext(request); + + const current = await getRecord(context, 'CredentialIssuer', name); + assert.strictEqual(current.namespace === gatewayId, true, 'Issuer invalid'); + assert.strictEqual(current === null, false, 'Issuer not found'); + + return await deleteRecord(context, 'CredentialIssuer', name); + } +} diff --git a/src/controllers/v3/OrgDatasetController.ts b/src/controllers/v3/OrgDatasetController.ts new file mode 100644 index 000000000..698ea58c4 --- /dev/null +++ b/src/controllers/v3/OrgDatasetController.ts @@ -0,0 +1,208 @@ +import { + Controller, + Request, + OperationId, + Put, + Path, + Route, + Security, + Body, + Get, + Tags, + Delete, +} from 'tsoa'; +import { strict as assert } from 'assert'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; +import { + syncRecordsThrowErrors, + getRecords, + parseJsonString, + removeEmpty, + removeKeys, + transformAllRefID, + deleteRecord, +} from '../../batch/feed-worker'; +import { BatchResult } from '../../batch/types'; +import { Dataset, DraftDataset } from './types'; + +@injectable() +@Route('/organizations') +@Tags('API Directory') +export class OrgDatasetController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + /** + * Get metadata about Datasets that are available by API for this organization + * > `Required Scope:` Dataset.Manage + * + * @summary Get Organization Datasets + */ + @Get('/{org}/datasets') + @OperationId('organization-datasets') + @Security('jwt', ['Dataset.Manage']) + public async getDatasets( + @Path() org: string, + @Request() request: any + ): Promise { + const ctx = this.keystone.createContext(request); + + const batchClause = { + query: '$org: String', + clause: '{ organization: { name: $org } }', + variables: { org }, + }; + + const records = await getRecords( + ctx, + 'DraftDataset', + undefined, + [], + batchClause + ); + + return records + .map((o) => removeEmpty(o)) + .map((o) => transformAllRefID(o, ['organization', 'organizationUnit'])) + .map((o) => parseJsonString(o, ['tags'])) + .map((o) => + removeKeys(o, [ + 'id', + 'namespace', + 'extSource', + 'extRecordHash', + 'extForeignKey', + ]) + ); + } + + /** + * Manage metadata about Datasets that are available by API for this organization + * > `Required Scope:` Dataset.Manage + * + * @summary Manage Organization Datasets + */ + @Put('{org}/datasets') + @OperationId('put-organization-dataset') + @Security('jwt', ['Dataset.Manage']) + public async putDataset( + @Path() org: string, + @Body() body: DraftDataset, + @Request() request: any + ): Promise { + assert.strictEqual(org, body['organization'], 'Organization Mismatch'); + return await syncRecordsThrowErrors( + this.keystone.createContext(request, true), + 'DraftDataset', + body['name'], + body + ); + } + + /** + * Delete a Dataset + * > `Required Scope:` Dataset.Manage + * + * @summary Delete a dataset + * @param ns + * @param appId + * @param request + * @returns + */ + @Delete('/{org}/datasets/{name}') + @OperationId('delete-dataset') + @Security('jwt', ['Dataset.Manage']) + public async delete( + @Path() org: string, + @Path() name: string, + @Request() request: any + ): Promise { + const context = this.keystone.createContext(request, true); + + const records = await getRecords( + context, + 'DraftDataset', + undefined, + ['organization', 'organizationUnit'], + { + query: '$org: String!, $name: String!', + clause: '{ organization: { name: $org }, name: $name }', + variables: { org, name }, + } + ); + + assert.strictEqual(records.length == 0, false, 'Dataset not found'); + + return await deleteRecord(context, 'DraftDataset', records.pop().name); + } + + /** + * Get metadata about a Dataset that are available by API for this organization + * > `Required Scope:` Dataset.Manage + * + * @summary Get Organization Dataset + */ + @Get('/{org}/datasets/{name}') + @OperationId('get-organization-dataset') + @Security('jwt', ['Dataset.Manage']) + public async getDataset( + @Path() org: string, + @Path() name: string, + @Request() request: any + ): Promise { + const ctx = this.keystone.createContext(request); + + const records = await getRecords( + ctx, + 'DraftDataset', + undefined, + ['organization', 'organizationUnit'], + { + query: '$org: String!, $name: String!', + clause: '{ organization: { name: $org }, name: $name }', + variables: { org, name }, + } + ); + + return records + .map((o) => removeEmpty(o)) + .map((o) => transformAllRefID(o, [])) + .map((o) => parseJsonString(o, ['tags', 'contacts', 'resources'])) + .map((o) => + removeKeys(o, [ + 'id', + 'namespace', + 'extSource', + 'extForeignKey', + 'extRecordHash', + 'orgUnits', + ]) + ) + .map((o) => transformContacts(o)) + .map((o) => transformResources(o)) + .pop(); + } +} + +export function transformResources(o: any) { + o.resources = o.resources?.map((res: any) => ({ + name: res.name, + url: res.url, + bcdc_type: res.bcdc_type, + format: res.format, + })); + return o; +} + +export function transformContacts(o: any) { + o.contacts = o.contacts?.map((con: any) => ({ + role: con.role, + name: con.name, + email: con.email, + })); + return o; +} diff --git a/src/controllers/v3/OrgRoleController.ts b/src/controllers/v3/OrgRoleController.ts new file mode 100644 index 000000000..faaece205 --- /dev/null +++ b/src/controllers/v3/OrgRoleController.ts @@ -0,0 +1,12 @@ +import { Controller, Get, Path, Route, Tags } from 'tsoa'; + +import { PredefinedRolePermissions } from '../../services/org-groups'; + +@Route('roles') +@Tags('Organizations') +export class OrgRoleController extends Controller { + @Get() + public async getRoles(): Promise { + return PredefinedRolePermissions; + } +} diff --git a/src/controllers/v3/OrganizationController.ts b/src/controllers/v3/OrganizationController.ts new file mode 100644 index 000000000..ab7585daf --- /dev/null +++ b/src/controllers/v3/OrganizationController.ts @@ -0,0 +1,272 @@ +import { + Controller, + Request, + Delete, + OperationId, + Put, + Path, + Route, + Query, + Security, + Body, + Get, + Tags, +} from 'tsoa'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { assertEqual } from '../ioc/assert'; +import { inject, injectable } from 'tsyringe'; +import { + syncRecords, + getRecords, + parseJsonString, + removeEmpty, + removeKeys, + transformAllRefID, +} from '../../batch/feed-worker'; +import { + GroupAccessService, + leaf, + NamespaceService, +} from '../../services/org-groups'; +import { + getGwaProductEnvironment, + transformActivity, +} from '../../services/workflow'; +import { + GroupAccess, + GroupMembership, + OrgNamespace, +} from '../../services/org-groups/types'; +import { getOrganizations, getOrganizationUnit } from '../../services/keystone'; +import { getActivity } from '../../services/keystone/activity'; +import { Activity } from './types'; +import { isParent } from '../../services/org-groups/group-converter-utils'; +import { ActivitySummary } from '../../services/keystone/types'; +import { ActivityDetail } from './types-extra'; + +@injectable() +@Route('/organizations') +@Tags('Organizations') +export class OrganizationController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + @Get() + @OperationId('organization-list') + public async listOrganizations(): Promise { + const orgs = await getOrganizations(this.keystone.sudo()); + return orgs.map((o) => ({ + name: o.name, + title: o.title, + description: o.description, + })); + } + + @Get('{org}') + @OperationId('organization-units') + public async listOrganizationUnits(@Path() org: string): Promise { + const orgs = await getOrganizations(this.keystone.sudo()); + const match = orgs.filter((o) => o.name === org).pop(); + assertEqual( + typeof match === 'undefined', + false, + 'org', + 'Organization not found.' + ); + + return { + orgUnits: match.orgUnits.map((o) => ({ + name: o.name, + title: o.title, + description: o.description, + })), + }; + } + + /** + * > `Required Scope:` GroupAccess.Manage + */ + @Get('{org}/roles') + @OperationId('get-organization-roles') + @Security('jwt', ['GroupAccess.Manage']) + public async getPolicies(@Path() org: string): Promise { + const prodEnv = await getGwaProductEnvironment(this.keystone.sudo(), false); + const envConfig = prodEnv.issuerEnvConfig; + + const groupAccessService = new GroupAccessService(prodEnv.uma2); + await groupAccessService.login(envConfig.clientId, envConfig.clientSecret); + + return await groupAccessService.getGroupAccess(org); + } + + /** + * > `Required Scope:` GroupAccess.Manage + */ + @Get('{org}/access') + @OperationId('get-organization-access') + @Security('jwt', ['GroupAccess.Manage']) + public async get(@Path() org: string): Promise { + const prodEnv = await getGwaProductEnvironment(this.keystone.sudo(), false); + const envConfig = prodEnv.issuerEnvConfig; + + const groupAccessService = new GroupAccessService(prodEnv.uma2); + await groupAccessService.login(envConfig.clientId, envConfig.clientSecret); + + return await groupAccessService.getGroupMembership(org); + } + + /** + * > `Required Scope:` GroupAccess.Manage + */ + @Put('{org}/access') + @OperationId('put-organization-access') + @Security('jwt', ['GroupAccess.Manage']) + public async put( + @Path() org: string, + @Body() body: GroupMembership + ): Promise { + // must match either the 'name' or one of the parent nodes + assertEqual( + org === body.name || isParent(body.parent, org), + true, + 'org', + 'Organization mismatch' + ); + + const prodEnv = await getGwaProductEnvironment(this.keystone.sudo(), false); + const envConfig = prodEnv.issuerEnvConfig; + + const groupAccessService = new GroupAccessService(prodEnv.uma2); + await groupAccessService.login(envConfig.clientId, envConfig.clientSecret); + + await groupAccessService.createOrUpdateGroupAccess(body, ['idir']); + } + + /** + * > `Required Scope:` Namespace.Assign + */ + @Get('{org}/gateways') + @OperationId('organization-gateways') + @Security('jwt', ['Namespace.Assign']) + public async listNamespaces(@Path() org: string): Promise { + const prodEnv = await getGwaProductEnvironment(this.keystone.sudo(), false); + const envConfig = prodEnv.issuerEnvConfig; + + const svc = new NamespaceService(envConfig.issuerUrl); + await svc.login(envConfig.clientId, envConfig.clientSecret); + return await svc.listAssignedNamespacesByOrg(org); + } + + /** + * > `Required Scope:` Namespace.Assign + */ + @Put('{org}/{orgUnit}/gateways/{gatewayId}') + @OperationId('assign-namespace-to-organization') + @Security('jwt', ['Namespace.Assign']) + public async assignNamespace( + @Path() org: string, + @Path() orgUnit: string, + @Path() gatewayId: string, + @Query() enable: boolean = true + ): Promise<{ result: string }> { + const ns = gatewayId; + const ctx = this.keystone.sudo(); + const orgLookup = await getOrganizationUnit(ctx, orgUnit); + assertEqual( + orgLookup != null && orgLookup.name === org, + true, + 'org', + 'Invalid Organization' + ); + + const prodEnv = await getGwaProductEnvironment(ctx, false); + const envConfig = prodEnv.issuerEnvConfig; + + const svc = new GroupAccessService(prodEnv.uma2); + await svc.login(envConfig.clientId, envConfig.clientSecret); + const answer = await svc.assignNamespace(ns, org, orgUnit, enable); + return { + result: answer + ? 'namespace-assigned' + : 'no-update-namespace-already-assigned', + }; + } + + /** + * > `Required Scope:` Namespace.Assign + */ + @Delete('{org}/{orgUnit}/gateways/{gatewayId}') + @OperationId('unassign-namespace-from-organization') + @Security('jwt', ['Namespace.Assign']) + public async unassignNamespace( + @Path() org: string, + @Path() orgUnit: string, + @Path() gatewayId: string + ): Promise<{ result: string }> { + const ns = gatewayId; + const ctx = this.keystone.sudo(); + const orgLookup = await getOrganizationUnit(ctx, orgUnit); + assertEqual( + orgLookup != null && orgLookup.name === org, + true, + 'org', + 'Invalid Organization' + ); + + const prodEnv = await getGwaProductEnvironment(ctx, false); + const envConfig = prodEnv.issuerEnvConfig; + + const svc = new GroupAccessService(prodEnv.uma2); + await svc.login(envConfig.clientId, envConfig.clientSecret); + const answer = await svc.unassignNamespace(ns, org, orgUnit); + return { + result: answer + ? 'namespace-unassigned' + : 'no-update-namespace-not-assigned', + }; + } + + /** + * > `Required Scope:` Namespace.Assign + * + * @summary Get Namespace Activity for gateways associated with this Organization Unit + * @param orgUnit + * @param first + * @param skip + * @returns Activity[] + */ + @Get('{org}/activity') + @OperationId('org-namespace-activity') + @Security('jwt', ['Namespace.Assign']) + public async namespaceActivity( + @Path() org: string, + @Query() first: number = 20, + @Query() skip: number = 0 + ): Promise { + const ctx = this.keystone.sudo(); + //const org = await getOrganizationUnit(ctx, orgUnit); + //assert.strictEqual(org != null, true, 'Invalid Organization Unit'); + + const prodEnv = await getGwaProductEnvironment(ctx, false); + const envConfig = prodEnv.issuerEnvConfig; + + const svc = new NamespaceService(envConfig.issuerUrl); + await svc.login(envConfig.clientId, envConfig.clientSecret); + const assignedNamespaces = await svc.listAssignedNamespacesByOrg(org); + const records = await getActivity( + ctx, + assignedNamespaces.map((n) => n.name), + undefined, + first > 100 ? 100 : first, + skip + ); + + return transformActivity(records) + .map((o) => removeEmpty(o)) + .map((o) => transformAllRefID(o, ['blob'])) + .map((o) => parseJsonString(o, ['blob'])); + } +} diff --git a/src/controllers/v3/ProductController.ts b/src/controllers/v3/ProductController.ts new file mode 100644 index 000000000..d78484ee7 --- /dev/null +++ b/src/controllers/v3/ProductController.ts @@ -0,0 +1,217 @@ +import { + Controller, + Request, + Delete, + Query, + OperationId, + Get, + Put, + Path, + Route, + Security, + Body, + Tags, + FieldErrors, + ValidateError, +} from 'tsoa'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; +import { + syncRecordsThrowErrors, + getRecords, + removeEmpty, + removeKeys, + transformAllRefID, + deleteRecord, + getRecord, + transformArrayKeyToString, +} from '../../batch/feed-worker'; +import { Product } from './types'; +import { BatchResult } from '../../batch/types'; + +import { Logger } from '../../logger'; +import { gql } from 'graphql-request'; +import { strict as assert } from 'assert'; +import { isEnvironmentID, isProductID } from '../../services/identifiers'; +import { Product as KSProduct } from '../../services/keystone/types'; + +const logger = Logger('controllers.Product'); + +@injectable() +@Route('/gateways/{gatewayId}') +@Tags('Products') +export class ProductController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + /** + * Manage Products for APIs that will appear on the API Directory + * > `Required Scope:` Namespace.Manage + * + * @summary Manage Products + * @param ns + * @param body + * @param request + */ + @Put('/products') + @OperationId('put-product') + @Security('jwt', ['Namespace.Manage']) + public async put( + @Path() gatewayId: string, + @Body() body: Product, + @Request() request: any + ): Promise { + body['gatewayId'] = gatewayId; + return await syncRecordsThrowErrors( + this.keystone.createContext(request), + 'Product', + body['appId'], + body + ); + } + + /** + * Get Products describing APIs that will appear on the API Directory + * > `Required Scope:` Namespace.Manage + * + * @summary Get Products + * @param ns + * @param request + * @returns + */ + @Get('/products') + @OperationId('get-products') + @Security('jwt', ['Namespace.Manage']) + public async get(@Request() request: any): Promise { + const ctx = this.keystone.createContext(request); + const records: KSProduct[] = await getRecords( + ctx, + 'Product', + 'allProductsByNamespace', + ['environments'] + ); + + return records + .map((o) => removeEmpty(o)) + .map((o) => + transformAllRefID(o, ['credentialIssuer', 'dataset', 'legal']) + ) + .map((o) => transformArrayKeyToString(o, 'services', 'name')) + .map((o) => + removeKeys(o, [ + 'id', + 'namespace', + 'product', + 'extSource', + 'extRecordHash', + 'extForeignKey', + ]) + ); + } + + /** + * Delete a Product + * > `Required Scope:` Namespace.Manage + * + * @summary Manage Products + * @param ns + * @param appId + * @param request + * @returns + */ + @Delete('/products/{appId}') + @OperationId('delete-product') + @Security('jwt', ['Namespace.Manage']) + public async delete( + @Path() gatewayId: string, + @Path() appId: string, + @Request() request: any + ): Promise { + const context = this.keystone.createContext(request); + + assert.strictEqual(isProductID(appId), true, 'Invalid appId'); + + const current = await getRecord(context, 'Product', appId); + assert.strictEqual(current === null, false, 'Product not found'); + assert.strictEqual( + current.namespace === gatewayId, + true, + 'Product invalid' + ); + return await deleteRecord(context, 'Product', appId); + } + + /** + * Delete a Product Environment + * > `Required Scope:` Namespace.Manage + * + * @summary Delete a Product Environment + * @param ns + * @param appId + * @param force + * @param request + * @returns + */ + @Delete('/environments/{appId}') + @OperationId('delete-product-environment') + @Security('jwt', ['Namespace.Manage']) + public async deleteEnvironment( + @Path() gatewayId: string, + @Path() appId: string, + @Query() force: boolean = false, + @Request() request: any + ): Promise { + const context = this.keystone.createContext(request); + + assert.strictEqual(isEnvironmentID(appId), true, 'Invalid appId'); + + const records: KSProduct[] = await getRecords( + context, + 'Product', + 'allProductsByNamespace', + ['environments'] + ); + const product = records + .filter((p) => p.environments.filter((e) => e.appId === appId).length > 0) + .pop(); + + assert.strictEqual( + typeof product === 'undefined', + false, + 'Environment not found' + ); + assert.strictEqual( + product.namespace === gatewayId, + true, + 'Environment invalid' + ); + + const environment = product.environments + .filter((e) => e.appId === appId) + .pop(); + + const result = await this.keystone.executeGraphQL({ + context, + query: deleteEnvironment, + variables: { prodEnvId: environment.id, force }, + }); + logger.debug('Result %j', result); + if (result.errors) { + const errors: FieldErrors = {}; + result.errors.forEach((err: any, ind: number) => { + errors[`d${ind}`] = { message: err.message }; + }); + throw new ValidateError(errors, 'Unable to delete product environment'); + } + return result.data.forceDeleteEnvironment; + } +} + +const deleteEnvironment = gql` + mutation ForceDeleteEnvironment($prodEnvId: ID!, $force: Boolean!) { + forceDeleteEnvironment(id: $prodEnvId, force: $force) + } +`; diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml new file mode 100644 index 000000000..0210e15a3 --- /dev/null +++ b/src/controllers/v3/openapi.yaml @@ -0,0 +1,1562 @@ +components: + examples: {} + headers: {} + parameters: {} + requestBodies: {} + responses: {} + schemas: + OrganizationRefID: + type: string + OrganizationUnitRefID: + type: string + Dataset: + properties: + extForeignKey: + type: string + name: + type: string + license_title: + type: string + security_class: + type: string + view_audience: + type: string + download_audience: + type: string + record_publish_date: + type: string + notes: + type: string + title: + type: string + isInCatalog: + type: string + isDraft: + type: string + contacts: + type: string + extSource: + type: string + extRecordHash: + type: string + tags: + items: + type: string + type: array + resources: {} + organization: + $ref: '#/components/schemas/OrganizationRefID' + organizationUnit: + $ref: '#/components/schemas/OrganizationUnitRefID' + type: object + additionalProperties: false + BatchResult: + properties: + status: + type: number + format: double + result: + type: string + reason: + type: string + id: + type: string + ownedBy: + type: string + childResults: + items: + $ref: '#/components/schemas/BatchResult' + type: array + required: + - status + - result + type: object + additionalProperties: false + DraftDataset: + properties: + name: + type: string + license_title: + type: string + security_class: + type: string + enum: + - HIGH-CABINET + - HIGH-CONFIDENTIAL + - HIGH-SENSITIVITY + - MEDIUM-SENSITIVITY + - MEDIUM-PERSONAL + - LOW-SENSITIVITY + - LOW-PUBLIC + - PUBLIC + - 'PROTECTED A' + - 'PROTECTED B' + - 'PROTECTED C' + view_audience: + type: string + enum: + - Public + - Government + - 'Named users' + - 'Government and Business BCeID' + download_audience: + type: string + enum: + - Public + - Government + - 'Named users' + - 'Government and Business BCeID' + record_publish_date: + type: string + notes: + type: string + title: + type: string + isInCatalog: + type: boolean + isDraft: + type: boolean + contacts: + type: string + resources: + type: string + tags: + items: + type: string + type: array + organization: + $ref: '#/components/schemas/OrganizationRefID' + organizationUnit: + $ref: '#/components/schemas/OrganizationUnitRefID' + type: object + additionalProperties: false + example: + name: my_sample_dataset + license_title: 'Open Government Licence - British Columbia' + security_class: PUBLIC + view_audience: Public + download_audience: Public + record_publish_date: '2017-09-05' + notes: 'Some notes' + title: 'A title about my dataset' + tags: + - tag1 + - tag2 + organization: ministry-of-citizens-services + organizationUnit: databc + Gateway: + properties: + gatewayId: + type: string + displayName: + type: string + type: object + additionalProperties: false + Maybe_Scalars-at-String_: + type: string + nullable: true + NamespaceInput: + properties: + displayName: + $ref: '#/components/schemas/Maybe_Scalars-at-String_' + name: + $ref: '#/components/schemas/Maybe_Scalars-at-String_' + type: object + ActivityDetail: + properties: + id: + type: string + message: + type: string + params: + properties: {} + additionalProperties: + type: string + type: object + activityAt: {} + blob: {} + required: + - message + - params + - activityAt + type: object + additionalProperties: false + PublishResult: + properties: + message: + type: string + results: + type: string + error: + type: string + type: object + additionalProperties: false + GatewayServiceRefID: + type: string + GatewayRouteRefID: + type: string + GatewayPlugin: + properties: + extForeignKey: + type: string + name: + type: string + extSource: + type: string + extRecordHash: + type: string + tags: + items: + type: string + type: array + config: {} + service: + $ref: '#/components/schemas/GatewayServiceRefID' + route: + $ref: '#/components/schemas/GatewayRouteRefID' + type: object + additionalProperties: false + GatewayRoute: + properties: + extForeignKey: + type: string + name: + type: string + gatewayId: + type: string + extSource: + type: string + extRecordHash: + type: string + tags: + items: + type: string + type: array + methods: + items: + type: string + type: array + paths: + items: + type: string + type: array + hosts: + items: + type: string + type: array + service: + $ref: '#/components/schemas/GatewayServiceRefID' + plugins: + items: + $ref: '#/components/schemas/GatewayPlugin' + type: array + type: object + additionalProperties: false + IssuerEnvironmentConfig: + properties: + environment: + type: string + exists: + type: boolean + issuerUrl: + type: string + clientRegistration: + type: string + enum: + - anonymous + - managed + - iat + clientId: + type: string + clientSecret: + type: string + initialAccessToken: + type: string + type: object + additionalProperties: false + example: + environment: dev + issuerUrl: 'https://idp.site/auth/realms/my-realm' + clientRegistration: managed + clientId: a-client-id + clientSecret: a-client-secret + undefinedRefID: + type: string + CredentialIssuer: + properties: + name: + type: string + gatewayId: + type: string + description: + type: string + flow: + type: string + enum: + - client-credentials + nullable: false + mode: + type: string + enum: + - auto + nullable: false + authPlugin: + type: string + clientAuthenticator: + type: string + enum: + - client-secret + - client-jwt + - client-jwt-jwks-url + instruction: + type: string + environmentDetails: + items: + $ref: '#/components/schemas/IssuerEnvironmentConfig' + type: array + resourceType: + type: string + resourceAccessScope: + type: string + isShared: + type: boolean + apiKeyName: + type: string + availableScopes: + items: + type: string + type: array + resourceScopes: + items: + type: string + type: array + clientRoles: + items: + type: string + type: array + clientMappers: + items: + type: string + type: array + inheritFrom: + $ref: '#/components/schemas/undefinedRefID' + owner: + $ref: '#/components/schemas/undefinedRefID' + type: object + additionalProperties: false + example: + name: my-auth-profile + description: 'Auth connection to my IdP' + flow: client-credentials + clientAuthenticator: client-secret + mode: auto + environmentDetails: [] + owner: janis@gov.bc.ca + GroupPermission: + properties: + resource: + type: string + scopes: + items: + type: string + type: array + required: + - scopes + type: object + additionalProperties: false + GroupRole: + properties: + name: + type: string + permissions: + items: + $ref: '#/components/schemas/GroupPermission' + type: array + required: + - name + - permissions + type: object + additionalProperties: false + GroupAccess: + properties: + name: + type: string + parent: + type: string + roles: + items: + $ref: '#/components/schemas/GroupRole' + type: array + required: + - roles + type: object + additionalProperties: false + UserReference: + properties: + id: + type: string + email: + type: string + type: object + additionalProperties: false + GroupMember: + properties: + member: + $ref: '#/components/schemas/UserReference' + roles: + items: + type: string + type: array + required: + - member + - roles + type: object + additionalProperties: false + GroupMembership: + properties: + name: + type: string + parent: + type: string + members: + items: + $ref: '#/components/schemas/GroupMember' + type: array + type: object + additionalProperties: false + OrgNamespace: + properties: + name: + type: string + orgUnit: + type: string + enabled: + type: boolean + updatedAt: + type: number + format: double + required: + - name + - orgUnit + - enabled + - updatedAt + type: object + additionalProperties: false + DraftDatasetRefID: + type: string + LegalRefID: + type: string + CredentialIssuerRefID: + type: string + Environment: + properties: + appId: + type: string + name: + type: string + enum: + - dev + - test + - prod + - sandbox + - other + active: + type: boolean + approval: + type: boolean + flow: + type: string + enum: + - public + - protected-externally + - authorization-code + - client-credentials + - kong-acl-only + - kong-api-key-only + - kong-api-key-acl + additionalDetailsToRequest: + type: string + services: + items: + $ref: '#/components/schemas/GatewayServiceRefID' + type: array + legal: + $ref: '#/components/schemas/LegalRefID' + credentialIssuer: + $ref: '#/components/schemas/CredentialIssuerRefID' + type: object + additionalProperties: false + example: + name: dev + active: false + approval: false + flow: public + appId: '00000000' + Product: + properties: + appId: + type: string + name: + type: string + description: + type: string + gatewayId: + type: string + dataset: + $ref: '#/components/schemas/DraftDatasetRefID' + environments: + items: + $ref: '#/components/schemas/Environment' + type: array + type: object + additionalProperties: false + example: + name: my-new-product + appId: '000000000000' + environments: + - + name: dev + active: false + approval: false + flow: public + appId: '00000000' + securitySchemes: + jwt: + type: oauth2 + description: 'Authz Client Credential' + flows: + clientCredentials: + tokenUrl: 'https://token_endpoint' + scopes: {} + portal: + type: http + description: 'Authz Portal Login' + scheme: bearer + bearerFormat: JWT + openid: + type: openIdConnect + description: 'OIDC Login' + openIdConnectUrl: 'https://well_known_endpoint' +info: + title: 'APS Directory API' + version: 3.0.0 + description: 'API Services Portal by BC Gov API Programme Services' + license: + name: MIT + contact: + name: 'BC Gov APS' +openapi: 3.0.0 +paths: + '/organizations/{org}/datasets': + get: + operationId: organization-datasets + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Dataset' + type: array + description: "Get metadata about Datasets that are available by API for this organization\n> `Required Scope:` Dataset.Manage" + summary: 'Get Organization Datasets' + tags: + - 'API Directory' + security: + - + jwt: + - Dataset.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + put: + operationId: put-organization-dataset + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Manage metadata about Datasets that are available by API for this organization\n> `Required Scope:` Dataset.Manage" + summary: 'Manage Organization Datasets' + tags: + - 'API Directory' + security: + - + jwt: + - Dataset.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DraftDataset' + '/organizations/{org}/datasets/{name}': + delete: + operationId: delete-dataset + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Delete a Dataset\n> `Required Scope:` Dataset.Manage" + summary: 'Delete a dataset' + tags: + - 'API Directory' + security: + - + jwt: + - Dataset.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + - + in: path + name: name + required: true + schema: + type: string + get: + operationId: get-organization-dataset + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Dataset' + description: "Get metadata about a Dataset that are available by API for this organization\n> `Required Scope:` Dataset.Manage" + summary: 'Get Organization Dataset' + tags: + - 'API Directory' + security: + - + jwt: + - Dataset.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + - + in: path + name: name + required: true + schema: + type: string + /directory: + get: + operationId: directory-list + responses: + '200': + description: Ok + content: + application/json: + schema: {} + tags: + - 'API Directory' + security: [] + parameters: [] + '/directory/{id}': + get: + operationId: directory-item + responses: + '200': + description: Ok + content: + application/json: + schema: {} + tags: + - 'API Directory' + security: [] + parameters: + - + in: path + name: id + required: true + schema: + type: string + '/gateways/{gatewayId}/datasets': + put: + operationId: put-dataset + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Update metadata about a Dataset\n> `Required Scope:` Namespace.Manage" + summary: 'Update Dataset' + tags: + - 'API Directory' + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DraftDataset' + '/gateways/{gatewayId}/datasets/{name}': + get: + operationId: get-dataset + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Dataset' + description: "Get metadata about a Dataset\n> `Required Scope:` Namespace.Manage" + summary: 'Get Dataset' + tags: + - 'API Directory' + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: path + name: name + required: true + schema: + type: string + /gateways/report: + get: + operationId: report + responses: + '200': + description: Ok + content: + application/json: + schema: {} + tags: + - Gateways + security: + - + jwt: [] + parameters: + - + in: query + name: ids + required: false + schema: + default: '[]' + type: string + /gateways: + get: + operationId: gateway-list + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + type: string + type: array + summary: 'List of Gateway IDs' + tags: + - Gateways + security: + - + jwt: [] + parameters: [] + post: + operationId: create-gateway + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway' + description: 'Create a gateway' + summary: 'Create Gateway' + tags: + - Gateways + security: + - + jwt: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NamespaceInput' + '/gateways/{gatewayId}': + get: + operationId: namespace-profile + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway' + description: "Get details about the gateway, such as permissions for what the gateway is setup with.\n> `Required Scope:` Namespace.Manage" + summary: 'Gateway Summary' + tags: + - Gateways + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + delete: + operationId: delete-namespace + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway' + description: "Delete the gateway\n> `Required Scope:` Namespace.Manage" + summary: 'Delete Gateway' + tags: + - Gateways + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: query + name: force + required: false + schema: + default: false + type: boolean + '/gateways/{gatewayId}/activity': + get: + operationId: gateway-admin-activity + responses: + '200': + description: 'Activity[]' + content: + application/json: + schema: + items: + $ref: '#/components/schemas/ActivityDetail' + type: array + description: '> `Required Scope:` Namespace.View' + summary: 'Get administration activity for this gateway' + tags: + - Gateways + security: + - + jwt: + - Namespace.View + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: query + name: first + required: false + schema: + default: 20 + format: double + type: number + - + in: query + name: skip + required: false + schema: + default: 0 + format: double + type: number + '/gateways/{gatewayId}/directory/{id}': + get: + operationId: get-ns-directory-dataset + responses: + '200': + description: Ok + content: + application/json: + schema: {} + description: "Used primarily for \"Preview Mode\"\nGet a particular Dataset" + tags: + - 'API Directory' + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: path + name: id + required: true + schema: + type: string + '/gateways/{gatewayId}/directory': + get: + operationId: get-ns-directory + responses: + '200': + description: Ok + content: + application/json: + schema: {} + description: "Used primarily for \"Preview Mode\"\nList the datasets belonging to a particular namespace" + tags: + - 'API Directory' + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + '/gateways/{gatewayId}/services': + put: + operationId: publish-gateway-config + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/PublishResult' + tags: + - 'Gateway Services' + security: + - + jwt: + - Gateway.Config + parameters: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + dryRun: + type: string + configFile: + type: string + format: binary + required: + - dryRun + - configFile + get: + operationId: get-gateway-routes + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/GatewayRoute' + type: array + description: "Get a summary of your Gateway Services\n> `Required Scope:` Namespace.Manage" + summary: 'Get Gateway Services' + tags: + - 'Gateway Services' + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + '/identifiers/{type}': + get: + operationId: GetNewID + responses: + '200': + description: Ok + content: + application/json: + schema: + type: string + tags: + - 'New Identifiers' + security: [] + parameters: + - + in: path + name: type + required: true + schema: + type: string + enum: + - environment + - product + - application + '/gateways/{gatewayId}/issuers': + put: + operationId: put-issuer + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Create or Update Authorization Profiles\n> `Required Scope:` CredentialIssuer.Admin" + summary: 'Manage Authorization Profiles' + tags: + - 'Authorization Profiles' + security: + - + jwt: + - CredentialIssuer.Admin + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CredentialIssuer' + get: + operationId: get-issuers + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/CredentialIssuer' + type: array + description: "Get Authorization Profiles setup in this namespace\n> `Required Scope:` Namespace.Manage" + summary: 'Get Authorization Profiles' + tags: + - 'Authorization Profiles' + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + '/gateways/{gatewayId}/issuers/{name}': + delete: + operationId: delete-issuer + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Delete an Authorization Profile\n> `Required Scope:` CredentialIssuer.Admin" + summary: 'Delete Profile' + tags: + - 'Authorization Profiles' + security: + - + jwt: + - CredentialIssuer.Admin + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: path + name: name + required: true + schema: + type: string + /organizations: + get: + operationId: organization-list + responses: + '200': + description: Ok + content: + application/json: + schema: + items: {} + type: array + tags: + - Organizations + security: [] + parameters: [] + '/organizations/{org}': + get: + operationId: organization-units + responses: + '200': + description: Ok + content: + application/json: + schema: {} + tags: + - Organizations + security: [] + parameters: + - + in: path + name: org + required: true + schema: + type: string + '/organizations/{org}/roles': + get: + operationId: get-organization-roles + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/GroupAccess' + description: '> `Required Scope:` GroupAccess.Manage' + tags: + - Organizations + security: + - + jwt: + - GroupAccess.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + '/organizations/{org}/access': + get: + operationId: get-organization-access + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/GroupMembership' + description: '> `Required Scope:` GroupAccess.Manage' + tags: + - Organizations + security: + - + jwt: + - GroupAccess.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + put: + operationId: put-organization-access + responses: + '204': + description: 'No content' + description: '> `Required Scope:` GroupAccess.Manage' + tags: + - Organizations + security: + - + jwt: + - GroupAccess.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GroupMembership' + '/organizations/{org}/gateways': + get: + operationId: organization-gateways + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/OrgNamespace' + type: array + description: '> `Required Scope:` Namespace.Assign' + tags: + - Organizations + security: + - + jwt: + - Namespace.Assign + parameters: + - + in: path + name: org + required: true + schema: + type: string + '/organizations/{org}/{orgUnit}/gateways/{gatewayId}': + put: + operationId: assign-namespace-to-organization + responses: + '200': + description: Ok + content: + application/json: + schema: + properties: + result: {type: string} + required: + - result + type: object + description: '> `Required Scope:` Namespace.Assign' + tags: + - Organizations + security: + - + jwt: + - Namespace.Assign + parameters: + - + in: path + name: org + required: true + schema: + type: string + - + in: path + name: orgUnit + required: true + schema: + type: string + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: query + name: enable + required: false + schema: + default: true + type: boolean + delete: + operationId: unassign-namespace-from-organization + responses: + '200': + description: Ok + content: + application/json: + schema: + properties: + result: {type: string} + required: + - result + type: object + description: '> `Required Scope:` Namespace.Assign' + tags: + - Organizations + security: + - + jwt: + - Namespace.Assign + parameters: + - + in: path + name: org + required: true + schema: + type: string + - + in: path + name: orgUnit + required: true + schema: + type: string + - + in: path + name: gatewayId + required: true + schema: + type: string + '/organizations/{org}/activity': + get: + operationId: org-namespace-activity + responses: + '200': + description: 'Activity[]' + content: + application/json: + schema: + items: + $ref: '#/components/schemas/ActivityDetail' + type: array + description: '> `Required Scope:` Namespace.Assign' + summary: 'Get Namespace Activity for gateways associated with this Organization Unit' + tags: + - Organizations + security: + - + jwt: + - Namespace.Assign + parameters: + - + in: path + name: org + required: true + schema: + type: string + - + in: query + name: first + required: false + schema: + default: 20 + format: double + type: number + - + in: query + name: skip + required: false + schema: + default: 0 + format: double + type: number + /roles: + get: + operationId: GetRoles + responses: + '200': + description: Ok + content: + application/json: + schema: {} + tags: + - Organizations + security: [] + parameters: [] + '/gateways/{gatewayId}/products': + put: + operationId: put-product + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Manage Products for APIs that will appear on the API Directory\n> `Required Scope:` Namespace.Manage" + summary: 'Manage Products' + tags: + - Products + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + get: + operationId: get-products + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Product' + type: array + description: "Get Products describing APIs that will appear on the API Directory\n> `Required Scope:` Namespace.Manage" + summary: 'Get Products' + tags: + - Products + security: + - + jwt: + - Namespace.Manage + parameters: [] + '/gateways/{gatewayId}/products/{appId}': + delete: + operationId: delete-product + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Delete a Product\n> `Required Scope:` Namespace.Manage" + summary: 'Manage Products' + tags: + - Products + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: path + name: appId + required: true + schema: + type: string + '/gateways/{gatewayId}/environments/{appId}': + delete: + operationId: delete-product-environment + responses: + '204': + description: 'No content' + description: "Delete a Product Environment\n> `Required Scope:` Namespace.Manage" + summary: 'Delete a Product Environment' + tags: + - Products + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string + - + in: path + name: appId + required: true + schema: + type: string + - + in: query + name: force + required: false + schema: + default: false + type: boolean +servers: + - + url: /ds/api/v3 +tags: + - + name: 'API Directory' + description: 'Discover all the great BC Government APIs' + - + name: Organizations + description: 'Manage organizational access control' + - + name: Gateways + description: 'Get aggregated information about gateways' + - + name: 'Gateway Services' + description: 'View your Gateway Service details' + - + name: Products + description: 'Manage your Products and Environments for publishing to the API Directory' + - + name: 'Authorization Profiles' + description: 'Configure the integration to external Identity Providers' + - + name: Documentation + description: 'View public documentation and publish documentation for your APIs' diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts new file mode 100644 index 000000000..c8383682e --- /dev/null +++ b/src/controllers/v3/routes.ts @@ -0,0 +1,1591 @@ +/* tslint:disable */ +/* eslint-disable */ +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, HttpStatusCodeLiteral, TsoaResponse } from '@tsoa/runtime'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { OrgDatasetController } from './OrgDatasetController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { DirectoryController } from './DirectoryController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { DatasetController } from './DatasetController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { NamespaceController } from './GatewayController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { NamespaceDirectoryController } from './GatewayDirectoryController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { GatewayController } from './GatewayServicesController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { IdentifiersController } from './IdentifierController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { IssuerController } from './IssuerController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { OrganizationController } from './OrganizationController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { OrgRoleController } from './OrgRoleController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { ProductController } from './ProductController'; +import { expressAuthentication } from './../../auth/auth-tsoa'; +// @ts-ignore - no great way to install types from subpackage +const promiseAny = require('promise.any'); +import { iocContainer } from './../ioc'; +import { IocContainer, IocContainerFactory } from '@tsoa/runtime'; +import * as express from 'express'; +const multer = require('multer'); +const upload = multer(); + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + +const models: TsoaRoute.Models = { + "OrganizationRefID": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "OrganizationUnitRefID": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Dataset": { + "dataType": "refObject", + "properties": { + "extForeignKey": {"dataType":"string"}, + "name": {"dataType":"string"}, + "license_title": {"dataType":"string"}, + "security_class": {"dataType":"string"}, + "view_audience": {"dataType":"string"}, + "download_audience": {"dataType":"string"}, + "record_publish_date": {"dataType":"string"}, + "notes": {"dataType":"string"}, + "title": {"dataType":"string"}, + "isInCatalog": {"dataType":"string"}, + "isDraft": {"dataType":"string"}, + "contacts": {"dataType":"string"}, + "extSource": {"dataType":"string"}, + "extRecordHash": {"dataType":"string"}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + "resources": {"dataType":"any"}, + "organization": {"ref":"OrganizationRefID"}, + "organizationUnit": {"ref":"OrganizationUnitRefID"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "BatchResult": { + "dataType": "refObject", + "properties": { + "status": {"dataType":"double","required":true}, + "result": {"dataType":"string","required":true}, + "reason": {"dataType":"string"}, + "id": {"dataType":"string"}, + "ownedBy": {"dataType":"string"}, + "childResults": {"dataType":"array","array":{"dataType":"refObject","ref":"BatchResult"}}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DraftDataset": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string"}, + "license_title": {"dataType":"string"}, + "security_class": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["HIGH-CABINET"]},{"dataType":"enum","enums":["HIGH-CONFIDENTIAL"]},{"dataType":"enum","enums":["HIGH-SENSITIVITY"]},{"dataType":"enum","enums":["MEDIUM-SENSITIVITY"]},{"dataType":"enum","enums":["MEDIUM-PERSONAL"]},{"dataType":"enum","enums":["LOW-SENSITIVITY"]},{"dataType":"enum","enums":["LOW-PUBLIC"]},{"dataType":"enum","enums":["PUBLIC"]},{"dataType":"enum","enums":["PROTECTED A"]},{"dataType":"enum","enums":["PROTECTED B"]},{"dataType":"enum","enums":["PROTECTED C"]}]}, + "view_audience": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["Public"]},{"dataType":"enum","enums":["Government"]},{"dataType":"enum","enums":["Named users"]},{"dataType":"enum","enums":["Government and Business BCeID"]}]}, + "download_audience": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["Public"]},{"dataType":"enum","enums":["Government"]},{"dataType":"enum","enums":["Named users"]},{"dataType":"enum","enums":["Government and Business BCeID"]}]}, + "record_publish_date": {"dataType":"string"}, + "notes": {"dataType":"string"}, + "title": {"dataType":"string"}, + "isInCatalog": {"dataType":"boolean"}, + "isDraft": {"dataType":"boolean"}, + "contacts": {"dataType":"string"}, + "resources": {"dataType":"string"}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + "organization": {"ref":"OrganizationRefID"}, + "organizationUnit": {"ref":"OrganizationUnitRefID"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Gateway": { + "dataType": "refObject", + "properties": { + "gatewayId": {"dataType":"string"}, + "displayName": {"dataType":"string"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Maybe_Scalars-at-String_": { + "dataType": "refAlias", + "type": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "NamespaceInput": { + "dataType": "refAlias", + "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"displayName":{"ref":"Maybe_Scalars-at-String_"},"name":{"ref":"Maybe_Scalars-at-String_"}},"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ActivityDetail": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string"}, + "message": {"dataType":"string","required":true}, + "params": {"dataType":"nestedObjectLiteral","nestedProperties":{},"additionalProperties":{"dataType":"string"},"required":true}, + "activityAt": {"dataType":"any","required":true}, + "blob": {"dataType":"any"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "PublishResult": { + "dataType": "refObject", + "properties": { + "message": {"dataType":"string"}, + "results": {"dataType":"string"}, + "error": {"dataType":"string"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "GatewayServiceRefID": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "GatewayRouteRefID": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "GatewayPlugin": { + "dataType": "refObject", + "properties": { + "extForeignKey": {"dataType":"string"}, + "name": {"dataType":"string"}, + "extSource": {"dataType":"string"}, + "extRecordHash": {"dataType":"string"}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + "config": {"dataType":"any"}, + "service": {"ref":"GatewayServiceRefID"}, + "route": {"ref":"GatewayRouteRefID"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "GatewayRoute": { + "dataType": "refObject", + "properties": { + "extForeignKey": {"dataType":"string"}, + "name": {"dataType":"string"}, + "gatewayId": {"dataType":"string"}, + "extSource": {"dataType":"string"}, + "extRecordHash": {"dataType":"string"}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + "methods": {"dataType":"array","array":{"dataType":"string"}}, + "paths": {"dataType":"array","array":{"dataType":"string"}}, + "hosts": {"dataType":"array","array":{"dataType":"string"}}, + "service": {"ref":"GatewayServiceRefID"}, + "plugins": {"dataType":"array","array":{"dataType":"refObject","ref":"GatewayPlugin"}}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "IssuerEnvironmentConfig": { + "dataType": "refObject", + "properties": { + "environment": {"dataType":"string"}, + "exists": {"dataType":"boolean"}, + "issuerUrl": {"dataType":"string"}, + "clientRegistration": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["anonymous"]},{"dataType":"enum","enums":["managed"]},{"dataType":"enum","enums":["iat"]}]}, + "clientId": {"dataType":"string"}, + "clientSecret": {"dataType":"string"}, + "initialAccessToken": {"dataType":"string"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "undefinedRefID": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "CredentialIssuer": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string"}, + "gatewayId": {"dataType":"string"}, + "description": {"dataType":"string"}, + "flow": {"dataType":"enum","enums":["client-credentials"]}, + "mode": {"dataType":"enum","enums":["auto"]}, + "authPlugin": {"dataType":"string"}, + "clientAuthenticator": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["client-secret"]},{"dataType":"enum","enums":["client-jwt"]},{"dataType":"enum","enums":["client-jwt-jwks-url"]}]}, + "instruction": {"dataType":"string"}, + "environmentDetails": {"dataType":"array","array":{"dataType":"refObject","ref":"IssuerEnvironmentConfig"}}, + "resourceType": {"dataType":"string"}, + "resourceAccessScope": {"dataType":"string"}, + "isShared": {"dataType":"boolean"}, + "apiKeyName": {"dataType":"string"}, + "availableScopes": {"dataType":"array","array":{"dataType":"string"}}, + "resourceScopes": {"dataType":"array","array":{"dataType":"string"}}, + "clientRoles": {"dataType":"array","array":{"dataType":"string"}}, + "clientMappers": {"dataType":"array","array":{"dataType":"string"}}, + "inheritFrom": {"ref":"undefinedRefID"}, + "owner": {"ref":"undefinedRefID"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "GroupPermission": { + "dataType": "refObject", + "properties": { + "resource": {"dataType":"string"}, + "scopes": {"dataType":"array","array":{"dataType":"string"},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "GroupRole": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string","required":true}, + "permissions": {"dataType":"array","array":{"dataType":"refObject","ref":"GroupPermission"},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "GroupAccess": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string"}, + "parent": {"dataType":"string"}, + "roles": {"dataType":"array","array":{"dataType":"refObject","ref":"GroupRole"},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "UserReference": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string"}, + "email": {"dataType":"string"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "GroupMember": { + "dataType": "refObject", + "properties": { + "member": {"ref":"UserReference","required":true}, + "roles": {"dataType":"array","array":{"dataType":"string"},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "GroupMembership": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string"}, + "parent": {"dataType":"string"}, + "members": {"dataType":"array","array":{"dataType":"refObject","ref":"GroupMember"}}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "OrgNamespace": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string","required":true}, + "orgUnit": {"dataType":"string","required":true}, + "enabled": {"dataType":"boolean","required":true}, + "updatedAt": {"dataType":"double","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DraftDatasetRefID": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "LegalRefID": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "CredentialIssuerRefID": { + "dataType": "refAlias", + "type": {"dataType":"string","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Environment": { + "dataType": "refObject", + "properties": { + "appId": {"dataType":"string"}, + "name": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["dev"]},{"dataType":"enum","enums":["test"]},{"dataType":"enum","enums":["prod"]},{"dataType":"enum","enums":["sandbox"]},{"dataType":"enum","enums":["other"]}]}, + "active": {"dataType":"boolean"}, + "approval": {"dataType":"boolean"}, + "flow": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["public"]},{"dataType":"enum","enums":["protected-externally"]},{"dataType":"enum","enums":["authorization-code"]},{"dataType":"enum","enums":["client-credentials"]},{"dataType":"enum","enums":["kong-acl-only"]},{"dataType":"enum","enums":["kong-api-key-only"]},{"dataType":"enum","enums":["kong-api-key-acl"]}]}, + "additionalDetailsToRequest": {"dataType":"string"}, + "services": {"dataType":"array","array":{"dataType":"refAlias","ref":"GatewayServiceRefID"}}, + "legal": {"ref":"LegalRefID"}, + "credentialIssuer": {"ref":"CredentialIssuerRefID"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Product": { + "dataType": "refObject", + "properties": { + "appId": {"dataType":"string"}, + "name": {"dataType":"string"}, + "description": {"dataType":"string"}, + "gatewayId": {"dataType":"string"}, + "dataset": {"ref":"DraftDatasetRefID"}, + "environments": {"dataType":"array","array":{"dataType":"refObject","ref":"Environment"}}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +}; +const validationService = new ValidationService(models); + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + +export function RegisterRoutes(app: express.Router) { + // ########################################################################################################### + // NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look + // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa + // ########################################################################################################### + app.get('/ds/api/v3/organizations/:org/datasets', + authenticateMiddleware([{"jwt":["Dataset.Manage"]}]), + + async function OrgDatasetController_getDatasets(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrgDatasetController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getDatasets.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.put('/ds/api/v3/organizations/:org/datasets', + authenticateMiddleware([{"jwt":["Dataset.Manage"]}]), + + async function OrgDatasetController_putDataset(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"DraftDataset"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrgDatasetController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.putDataset.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.delete('/ds/api/v3/organizations/:org/datasets/:name', + authenticateMiddleware([{"jwt":["Dataset.Manage"]}]), + + async function OrgDatasetController_delete(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + name: {"in":"path","name":"name","required":true,"dataType":"string"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrgDatasetController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.delete.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/organizations/:org/datasets/:name', + authenticateMiddleware([{"jwt":["Dataset.Manage"]}]), + + async function OrgDatasetController_getDataset(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + name: {"in":"path","name":"name","required":true,"dataType":"string"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrgDatasetController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getDataset.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/directory', + + async function DirectoryController_list(request: any, response: any, next: any) { + const args = { + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(DirectoryController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.list.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/directory/:id', + + async function DirectoryController_get(request: any, response: any, next: any) { + const args = { + id: {"in":"path","name":"id","required":true,"dataType":"string"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(DirectoryController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.get.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.put('/ds/api/v3/gateways/:gatewayId/datasets', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function DatasetController_put(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"DraftDataset"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(DatasetController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.put.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/gateways/:gatewayId/datasets/:name', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function DatasetController_getDataset(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + name: {"in":"path","name":"name","required":true,"dataType":"string"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(DatasetController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getDataset.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/gateways/report', + authenticateMiddleware([{"jwt":[]}]), + + async function NamespaceController_report(request: any, response: any, next: any) { + const args = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + ids: {"default":"[]","in":"query","name":"ids","dataType":"string"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(NamespaceController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.report.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/gateways', + authenticateMiddleware([{"jwt":[]}]), + + async function NamespaceController_list(request: any, response: any, next: any) { + const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(NamespaceController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.list.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/gateways/:gatewayId', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function NamespaceController_profile(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(NamespaceController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.profile.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.post('/ds/api/v3/gateways', + authenticateMiddleware([{"jwt":[]}]), + + async function NamespaceController_create(request: any, response: any, next: any) { + const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + vars: {"in":"body","name":"vars","required":true,"ref":"NamespaceInput"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(NamespaceController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.create.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.delete('/ds/api/v3/gateways/:gatewayId', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function NamespaceController_delete(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + force: {"default":false,"in":"query","name":"force","dataType":"boolean"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(NamespaceController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.delete.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/gateways/:gatewayId/activity', + authenticateMiddleware([{"jwt":["Namespace.View"]}]), + + async function NamespaceController_namespaceActivity(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + first: {"default":20,"in":"query","name":"first","dataType":"double"}, + skip: {"default":0,"in":"query","name":"skip","dataType":"double"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(NamespaceController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.namespaceActivity.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/gateways/:gatewayId/directory/:id', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function NamespaceDirectoryController_getDataset(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + id: {"in":"path","name":"id","required":true,"dataType":"string"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(NamespaceDirectoryController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getDataset.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/gateways/:gatewayId/directory', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function NamespaceDirectoryController_getDatasets(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(NamespaceDirectoryController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getDatasets.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.put('/ds/api/v3/gateways/:gatewayId/services', + authenticateMiddleware([{"jwt":["Gateway.Config"]}]), + upload.single('configFile'), + + async function GatewayController_put(request: any, response: any, next: any) { + const args = { + dryRun: {"in":"formData","name":"dryRun","required":true,"dataType":"string"}, + configFile: {"in":"formData","name":"configFile","required":true,"dataType":"file"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(GatewayController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.put.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/gateways/:gatewayId/services', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function GatewayController_get(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(GatewayController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.get.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/identifiers/:type', + + async function IdentifiersController_getNewID(request: any, response: any, next: any) { + const args = { + type: {"in":"path","name":"type","required":true,"dataType":"union","subSchemas":[{"dataType":"enum","enums":["environment"]},{"dataType":"enum","enums":["product"]},{"dataType":"enum","enums":["application"]}]}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(IdentifiersController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getNewID.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.put('/ds/api/v3/gateways/:gatewayId/issuers', + authenticateMiddleware([{"jwt":["CredentialIssuer.Admin"]}]), + + async function IssuerController_put(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"CredentialIssuer"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(IssuerController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.put.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/gateways/:gatewayId/issuers', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function IssuerController_get(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(IssuerController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.get.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.delete('/ds/api/v3/gateways/:gatewayId/issuers/:name', + authenticateMiddleware([{"jwt":["CredentialIssuer.Admin"]}]), + + async function IssuerController_delete(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + name: {"in":"path","name":"name","required":true,"dataType":"string"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(IssuerController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.delete.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/organizations', + + async function OrganizationController_listOrganizations(request: any, response: any, next: any) { + const args = { + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.listOrganizations.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/organizations/:org', + + async function OrganizationController_listOrganizationUnits(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.listOrganizationUnits.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/organizations/:org/roles', + authenticateMiddleware([{"jwt":["GroupAccess.Manage"]}]), + + async function OrganizationController_getPolicies(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getPolicies.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/organizations/:org/access', + authenticateMiddleware([{"jwt":["GroupAccess.Manage"]}]), + + async function OrganizationController_get(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.get.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.put('/ds/api/v3/organizations/:org/access', + authenticateMiddleware([{"jwt":["GroupAccess.Manage"]}]), + + async function OrganizationController_put(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"GroupMembership"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.put.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/organizations/:org/gateways', + authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), + + async function OrganizationController_listNamespaces(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.listNamespaces.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.put('/ds/api/v3/organizations/:org/:orgUnit/gateways/:gatewayId', + authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), + + async function OrganizationController_assignNamespace(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + orgUnit: {"in":"path","name":"orgUnit","required":true,"dataType":"string"}, + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + enable: {"default":true,"in":"query","name":"enable","dataType":"boolean"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.assignNamespace.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.delete('/ds/api/v3/organizations/:org/:orgUnit/gateways/:gatewayId', + authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), + + async function OrganizationController_unassignNamespace(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + orgUnit: {"in":"path","name":"orgUnit","required":true,"dataType":"string"}, + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.unassignNamespace.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/organizations/:org/activity', + authenticateMiddleware([{"jwt":["Namespace.Assign"]}]), + + async function OrganizationController_namespaceActivity(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + first: {"default":20,"in":"query","name":"first","dataType":"double"}, + skip: {"default":0,"in":"query","name":"skip","dataType":"double"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.namespaceActivity.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/roles', + + async function OrgRoleController_getRoles(request: any, response: any, next: any) { + const args = { + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrgRoleController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.getRoles.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.put('/ds/api/v3/gateways/:gatewayId/products', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function ProductController_put(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"Product"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(ProductController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.put.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/gateways/:gatewayId/products', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function ProductController_get(request: any, response: any, next: any) { + const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(ProductController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.get.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.delete('/ds/api/v3/gateways/:gatewayId/products/:appId', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function ProductController_delete(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + appId: {"in":"path","name":"appId","required":true,"dataType":"string"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(ProductController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.delete.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.delete('/ds/api/v3/gateways/:gatewayId/environments/:appId', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function ProductController_deleteEnvironment(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + appId: {"in":"path","name":"appId","required":true,"dataType":"string"}, + force: {"default":false,"in":"query","name":"force","dataType":"boolean"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(ProductController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.deleteEnvironment.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + function authenticateMiddleware(security: TsoaRoute.Security[] = []) { + return async function runAuthenticationMiddleware(request: any, _response: any, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + // keep track of failed auth attempts so we can hand back the most + // recent one. This behavior was previously existing so preserving it + // here + const failedAttempts: any[] = []; + const pushAndRethrow = (error: any) => { + failedAttempts.push(error); + throw error; + }; + + const secMethodOrPromises: Promise[] = []; + for (const secMethod of security) { + if (Object.keys(secMethod).length > 1) { + const secMethodAndPromises: Promise[] = []; + + for (const name in secMethod) { + secMethodAndPromises.push( + expressAuthentication(request, name, secMethod[name]) + .catch(pushAndRethrow) + ); + } + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + secMethodOrPromises.push(Promise.all(secMethodAndPromises) + .then(users => { return users[0]; })); + } else { + for (const name in secMethod) { + secMethodOrPromises.push( + expressAuthentication(request, name, secMethod[name]) + .catch(pushAndRethrow) + ); + } + } + } + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + try { + request['user'] = await promiseAny(secMethodOrPromises); + next(); + } + catch(err) { + // Show most recent error as response + const error = failedAttempts.pop(); + error.status = error.status || 401; + next(error); + } + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + } + } + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + function isController(object: any): object is Controller { + return 'getHeaders' in object && 'getStatus' in object && 'setStatus' in object; + } + + function promiseHandler(controllerObj: any, promise: any, response: any, successStatus: any, next: any) { + return Promise.resolve(promise) + .then((data: any) => { + let statusCode = successStatus; + let headers; + if (isController(controllerObj)) { + headers = controllerObj.getHeaders(); + statusCode = controllerObj.getStatus() || statusCode; + } + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + returnHandler(response, statusCode, data, headers) + }) + .catch((error: any) => next(error)); + } + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + function returnHandler(response: any, statusCode?: number, data?: any, headers: any = {}) { + if (response.headersSent) { + return; + } + Object.keys(headers).forEach((name: string) => { + response.set(name, headers[name]); + }); + if (data && typeof data.pipe === 'function' && data.readable && typeof data._read === 'function') { + data.pipe(response); + } else if (data !== null && data !== undefined) { + response.status(statusCode || 200).json(data); + } else { + response.status(statusCode || 204).end(); + } + } + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + function responder(response: any): TsoaResponse { + return function(status, data, headers) { + returnHandler(response, status, data, headers); + }; + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + function getValidatedArgs(args: any, request: any, response: any): any[] { + const fieldErrors: FieldErrors = {}; + const values = Object.keys(args).map((key) => { + const name = args[key].name; + switch (args[key].in) { + case 'request': + return request; + case 'query': + return validationService.ValidateParam(args[key], request.query[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'path': + return validationService.ValidateParam(args[key], request.params[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'header': + return validationService.ValidateParam(args[key], request.header(name), name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'body': + return validationService.ValidateParam(args[key], request.body, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'body-prop': + return validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, 'body.', {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'formData': + if (args[key].dataType === 'file') { + return validationService.ValidateParam(args[key], request.file, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + } else if (args[key].dataType === 'array' && args[key].array.dataType === 'file') { + return validationService.ValidateParam(args[key], request.files, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + } else { + return validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + } + case 'res': + return responder(response); + } + }); + + if (Object.keys(fieldErrors).length > 0) { + throw new ValidateError(fieldErrors, ''); + } + return values; + } + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +} + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa diff --git a/src/controllers/v3/types-extra.ts b/src/controllers/v3/types-extra.ts new file mode 100644 index 000000000..438ddde0d --- /dev/null +++ b/src/controllers/v3/types-extra.ts @@ -0,0 +1,21 @@ +import { Scalars } from '../../services/keystone/types'; + +/** + * @tsoaModel + */ +export interface ActivityDetail { + id?: string; + message: string; + params: { [key: string]: string }; + activityAt: Scalars['DateTime']; + blob?: any; +} + +/** + * @tsoaModel + */ +export interface PublishResult { + message?: string; + results?: string; + error?: string; +} diff --git a/src/controllers/v3/types.ts b/src/controllers/v3/types.ts new file mode 100644 index 000000000..d59ce01d8 --- /dev/null +++ b/src/controllers/v3/types.ts @@ -0,0 +1,574 @@ + +/********************************************/ +/***** WARNING!!!! THIS IS AUTO-GENERATED ***/ +/***** RUNING: npm run tsoa-gen-types ***/ +/********************************************/ + +export type DateTime = any; + + +/** + * @tsoaModel + * + */ +export interface Organization { + extForeignKey?: string; // Primary Key + name?: string; + sector?: string; + title?: string; + description?: string; + extSource?: string; + extRecordHash?: string; + tags?: string[]; + orgUnits?: OrganizationUnit[]; +} + + +/** + * @tsoaModel + * + */ +export interface OrganizationUnit { + extForeignKey?: string; // Primary Key + name?: string; + sector?: string; + title?: string; + description?: string; + extSource?: string; + extRecordHash?: string; + tags?: string[]; +} + + +/** + * @tsoaModel + * + */ +export interface Dataset { + extForeignKey?: string; // Primary Key + name?: string; + license_title?: string; + security_class?: string; + view_audience?: string; + download_audience?: string; + record_publish_date?: string; + notes?: string; + title?: string; + isInCatalog?: string; + isDraft?: string; + contacts?: string; + extSource?: string; + extRecordHash?: string; + tags?: string[]; + resources?: any; // toString + organization?: OrganizationRefID; + organizationUnit?: OrganizationUnitRefID; +} + + +/** + * @tsoaModel + * @example { + * "name": "my_sample_dataset", + * "license_title": "Open Government Licence - British Columbia", + * "security_class": "PUBLIC", + * "view_audience": "Public", + * "download_audience": "Public", + * "record_publish_date": "2017-09-05", + * "notes": "Some notes", + * "title": "A title about my dataset", + * "tags": [ + * "tag1", + * "tag2" + * ], + * "organization": "ministry-of-citizens-services", + * "organizationUnit": "databc" + * } + */ +export interface DraftDataset { + name?: string; // Primary Key + license_title?: string; + security_class?: "HIGH-CABINET" | "HIGH-CONFIDENTIAL" | "HIGH-SENSITIVITY" | "MEDIUM-SENSITIVITY" | "MEDIUM-PERSONAL" | "LOW-SENSITIVITY" | "LOW-PUBLIC" | "PUBLIC" | "PROTECTED A" | "PROTECTED B" | "PROTECTED C"; + view_audience?: "Public" | "Government" | "Named users" | "Government and Business BCeID"; + download_audience?: "Public" | "Government" | "Named users" | "Government and Business BCeID"; + record_publish_date?: string; + notes?: string; + title?: string; + isInCatalog?: boolean; + isDraft?: boolean; + contacts?: string; + resources?: string; + tags?: string[]; + organization?: OrganizationRefID; + organizationUnit?: OrganizationUnitRefID; +} + + +/** + * @tsoaModel + * + */ +export interface Metric { + name?: string; // Primary Key + query?: string; + day?: string; + metric?: any; // toString + values?: any; // toString + service?: GatewayServiceRefID; +} + + +/** + * @tsoaModel + * + */ +export interface Alert { + name?: string; // Primary Key +} + + +/** + * @tsoaModel + * + */ +export interface Namespace { + extRefId?: string; // Primary Key + name?: string; + displayName?: string; +} + + +/** + * @tsoaModel + * + */ +export interface Gateway { + gatewayId?: string; // Primary Key + displayName?: string; +} + + +/** + * @tsoaModel + * + */ +export interface MemberRole { + extRefId?: string; // Primary Key + role?: string; + user?: UserRefID; +} + + +/** + * @tsoaModel + * + */ +export interface GatewayService { + extForeignKey?: string; // Primary Key + name?: string; + gatewayId?: string; + host?: string; + extSource?: string; + extRecordHash?: string; + tags?: string[]; + plugins?: GatewayPlugin[]; +} + + +/** + * @tsoaModel + * + */ +export interface GatewayGroup { + extForeignKey?: string; // Primary Key + name?: string; + gatewayId?: string; + extSource?: string; + extRecordHash?: string; +} + + +/** + * @tsoaModel + * + */ +export interface GatewayRoute { + extForeignKey?: string; // Primary Key + name?: string; + gatewayId?: string; + extSource?: string; + extRecordHash?: string; + tags?: string[]; + methods?: string[]; + paths?: string[]; + hosts?: string[]; + service?: GatewayServiceRefID; + plugins?: GatewayPlugin[]; +} + + +/** + * @tsoaModel + * + */ +export interface GatewayPlugin { + extForeignKey?: string; // Primary Key + name?: string; + extSource?: string; + extRecordHash?: string; + tags?: string[]; + config?: any; // toString + service?: GatewayServiceRefID; + route?: GatewayRouteRefID; +} + + +/** + * @tsoaModel + * + */ +export interface GatewayConsumer { + extForeignKey?: string; // Primary Key + username?: string; + customId?: string; + gatewayId?: string; + extSource?: string; + extRecordHash?: string; + tags?: string[]; + aclGroups?: string[]; + plugins?: GatewayPlugin[]; +} + + +/** + * @tsoaModel + * + */ +export interface ServiceAccess { + name?: string; // Primary Key + active?: string; + aclEnabled?: string; + consumerType?: string; + application?: ApplicationRefID; + consumer?: GatewayConsumerRefID; + productEnvironment?: EnvironmentRefID; +} + + +/** + * @tsoaModel + * + */ +export interface Application { + appId?: string; // Primary Key + name?: string; + description?: string; + owner?: UserRefID; + organization?: OrganizationRefID; + organizationUnit?: OrganizationUnitRefID; +} + + +/** + * @tsoaModel + * @example { + * "name": "my-new-product", + * "appId": "000000000000", + * "environments": [ + * { + * "name": "dev", + * "active": false, + * "approval": false, + * "flow": "public", + * "appId": "00000000" + * } + * ] + * } + */ +export interface Product { + appId?: string; // Primary Key + name?: string; + description?: string; + gatewayId?: string; + dataset?: DraftDatasetRefID; + environments?: Environment[]; +} + + +/** + * @tsoaModel + * @example { + * "name": "dev", + * "active": false, + * "approval": false, + * "flow": "public", + * "appId": "00000000" + * } + */ +export interface Environment { + appId?: string; // Primary Key + name?: "dev" | "test" | "prod" | "sandbox" | "other"; + active?: boolean; + approval?: boolean; + flow?: "public" | "protected-externally" | "authorization-code" | "client-credentials" | "kong-acl-only" | "kong-api-key-only" | "kong-api-key-acl"; + additionalDetailsToRequest?: string; + services?: GatewayServiceRefID[]; + legal?: LegalRefID; + credentialIssuer?: CredentialIssuerRefID; +} + + +/** + * @tsoaModel + * @example { + * "name": "my-auth-profile", + * "description": "Auth connection to my IdP", + * "flow": "client-credentials", + * "clientAuthenticator": "client-secret", + * "mode": "auto", + * "environmentDetails": [], + * "owner": "janis@gov.bc.ca" + * } + */ +export interface CredentialIssuer { + name?: string; // Primary Key + gatewayId?: string; + description?: string; + flow?: "client-credentials"; + mode?: "auto"; + authPlugin?: string; + clientAuthenticator?: "client-secret" | "client-jwt" | "client-jwt-jwks-url"; + instruction?: string; + environmentDetails?: IssuerEnvironmentConfig[]; + resourceType?: string; + resourceAccessScope?: string; + isShared?: boolean; + apiKeyName?: string; + availableScopes?: string[]; + resourceScopes?: string[]; + clientRoles?: string[]; + clientMappers?: string[]; + inheritFrom?: undefinedRefID; + owner?: undefinedRefID; +} + + +/** + * @tsoaModel + * @example { + * "environment": "dev", + * "issuerUrl": "https://idp.site/auth/realms/my-realm", + * "clientRegistration": "managed", + * "clientId": "a-client-id", + * "clientSecret": "a-client-secret" + * } + */ +export interface IssuerEnvironmentConfig { + environment?: string; // Primary Key + exists?: boolean; + issuerUrl?: string; + clientRegistration?: "anonymous" | "managed" | "iat"; + clientId?: string; + clientSecret?: string; + initialAccessToken?: string; +} + + +/** + * @tsoaModel + * @example { + * "externalLink": "https://externalsite/my_content", + * "title": "my_content", + * "description": "Summary of what my content is", + * "content": "Markdown content", + * "order": 0, + * "isPublic": true, + * "isComplete": true, + * "tags": [ + * "tag1", + * "tag2" + * ] + * } + */ +export interface Content { + externalLink?: string; // Primary Key + title?: string; + description?: string; + content?: string; + githubRepository?: string; + readme?: string; + order?: number; + isPublic?: boolean; + isComplete?: boolean; + gatewayId?: string; + publishDate?: string; + slug?: string; + tags?: string[]; +} + + +/** + * @tsoaModel + * + */ +export interface ContentBySlug { + slug?: string; // Primary Key + externalLink?: string; + title?: string; + description?: string; + content?: string; + githubRepository?: string; + readme?: string; + order?: string; + isPublic?: string; + isComplete?: string; + gatewayId?: string; + publishDate?: string; + tags?: string[]; +} + + +/** + * @tsoaModel + * + */ +export interface Legal { + reference?: string; // Primary Key + title?: string; + link?: string; + document?: string; + version?: string; + active?: string; +} + + +/** + * @tsoaModel + * @example { + * "type": "Namespace", + * "name": "delete Namespace[ns_x]", + * "action": "delete", + * "refId": "ns_x", + * "result": "success", + * "message": "Deleted ns_x namespace", + * "actor": { + * "name": "XT:Blink, James CITZ:IN" + * }, + * "blob": {}, + * "createdAt": "2022-03-11T00:47:42.947Z", + * "updatedAt": "2022-03-11T00:47:42.947Z" + * } + */ +export interface Activity { + extRefId?: string; // Primary Key + type?: string; + name?: string; + action?: "add" | "update" | "create" | "delete" | "validate" | "publish"; + result?: "" | "received" | "failed" | "completed" | "success"; + message?: string; + refId?: string; + gatewayId?: string; + blob?: string; + filterKey1?: string; + filterKey2?: string; + filterKey3?: string; + filterKey4?: string; + updatedAt?: DateTime; + createdAt?: DateTime; + context?: any; // toString + actor?: UserRefID; +} + + +/** + * @tsoaModel + * + */ +export interface User { + username?: string; // Primary Key + name?: string; + email?: string; + legalsAgreed?: UserLegalsAgreed[]; + provider?: string; +} + + +/** + * @tsoaModel + * + */ +export interface UserLegalsAgreed { + reference?: string; // Primary Key + agreedTimestamp?: string; +} + + +/** + * @tsoaModel + * + */ +export interface Blob { + ref?: string; // Primary Key + type?: string; + blob?: string; +} + +/** + * @tsoaModel + */ +export type ApplicationRefID = string + +/** + * @tsoaModel + */ +export type CredentialIssuerRefID = string + +/** + * @tsoaModel + */ +export type DraftDatasetRefID = string + +/** + * @tsoaModel + */ +export type EnvironmentRefID = string + +/** + * @tsoaModel + */ +export type GatewayConsumerRefID = string + +/** + * @tsoaModel + */ +export type GatewayRouteRefID = string + +/** + * @tsoaModel + */ +export type GatewayServiceRefID = string + +/** + * @tsoaModel + */ +export type LegalRefID = string + +/** + * @tsoaModel + */ +export type OrganizationRefID = string + +/** + * @tsoaModel + */ +export type OrganizationUnitRefID = string + +/** + * @tsoaModel + */ +export type UserRefID = string + +/** + * @tsoaModel + */ +export type undefinedRefID = string diff --git a/src/package.json b/src/package.json index 63a02ff6a..147f8aad3 100644 --- a/src/package.json +++ b/src/package.json @@ -27,6 +27,7 @@ "ts-build": "tsc", "tsoa-build-v1": "tsoa -c tsoa.json spec-and-routes", "tsoa-build-v2": "tsoa -c tsoa-v2.json spec-and-routes", + "tsoa-build-v3": "tsoa -c tsoa-v3.json spec-and-routes", "tsoa-gen-types": "ts-node tools/tsoaTypes", "delete-assets": "ts-node tools/deleteAssets", "copy-assets": "ts-node tools/copyAssets", @@ -36,13 +37,13 @@ "nextapp-dev": "cross-env NEXT_PUBLIC_MOCKS=on NODE_ENV=development NODE_OPTIONS='--openssl-legacy-provider --no-experimental-fetch --dns-result-order=ipv4first' next dev ./nextapp", "batch": "cross-env NODE_ENV=development node dist/server-batch.js", "intg-build": "cross-env NODE_ENV=development npm-run-all delete-assets copy-assets ts-build", - "dev": "cross-env NODE_ENV=development NODE_OPTIONS='--openssl-legacy-provider --no-experimental-fetch --dns-result-order=ipv4first' npm-run-all delete-assets copy-assets tsoa-gen-types tsoa-build-v1 tsoa-build-v2 ts-build ks-dev", + "dev": "cross-env NODE_ENV=development NODE_OPTIONS='--openssl-legacy-provider --no-experimental-fetch --dns-result-order=ipv4first' npm-run-all delete-assets copy-assets tsoa-gen-types tsoa-build-v1 tsoa-build-v2 tsoa-build-v3 ts-build ks-dev", "ks-dev": "cross-env NODE_ENV=development DISABLE_LOGGING=true keystone dev --entry=dist/server.js", "dev2": "cross-env NODE_ENV=development DISABLE_LOGGING=true keystone --entry=dist/index.js", "mock-server": "nodemon ./test/mock-server/server.js", "ks-build": "cross-env NODE_ENV=production keystone build --entry=dist/server.js --out=dist2", "start": "cross-env NODE_ENV=production keystone start --entry=dist/server.js", - "build": "NODE_OPTIONS='--dns-result-order=ipv4first' npm-run-all delete-assets copy-assets tsoa-build-v1 tsoa-build-v2 ts-build ks-build copy-keystone-admin-assets", + "build": "NODE_OPTIONS='--dns-result-order=ipv4first' npm-run-all delete-assets copy-assets tsoa-build-v1 tsoa-build-v2 tsoa-build-v3 ts-build ks-build copy-keystone-admin-assets", "create-tables": "cross-env CREATE_TABLES=true keystone create-tables --entry=dist/server.js", "lint": "eslint ./nextapp --ext .ts,.tsx --quiet", "lint:ts": "tsc -p ./nextapp/tsconfig.json --noEmit", diff --git a/src/tools/tsoaTypes.js b/src/tools/tsoaTypes.js index 059875d46..798400aed 100644 --- a/src/tools/tsoaTypes.js +++ b/src/tools/tsoaTypes.js @@ -2,7 +2,8 @@ const fs = require('fs'); const { metadata } = require('../batch/data-rules.js'); -let content = ` +function buildContent(convertToGatewayId) { + let content = ` /********************************************/ /***** WARNING!!!! THIS IS AUTO-GENERATED ***/ /***** RUNING: npm run tsoa-gen-types ***/ @@ -11,101 +12,112 @@ let content = ` export type DateTime = any; `; -const refIdList = {}; - -Object.keys(metadata).forEach(function (m) { - const md = metadata[m]; - - const relationshipFields = Object.keys( - md.transformations - ).filter((tranField) => - [ - 'connectOne', - 'connectExclusiveList', - 'connectExclusiveListCreate', - 'connectMany', - ].includes(md.transformations[tranField].name) - ); - - const objectFields = Object.keys(md.transformations).filter( - (tranField) => - ['toString', 'toStringDefaultArray'].includes( - md.transformations[tranField].name - ) && !Object.keys(md.validations || {}).includes(tranField) - ); - - const fields = []; - fields.push(` ${md.refKey}?: string; // Primary Key`); - md.sync - .concat(md.read ? md.read : []) - .filter((s) => !relationshipFields.includes(s)) - .filter((s) => !objectFields.includes(s)) - .filter((s) => s != md.refKey) - .slice() - .forEach((f) => { - let type = - md.validations && f in md.validations - ? md.validations[f].type - : 'string'; - if (type === 'enum') { - fields.push( - ` ${f}?: ${md.validations[f].values - .map((v) => `"${v}"`) - .join(' | ')};` - ); - } else if (type === 'entityArray') { - fields.push(` ${f}?: ${md.validations[f].entity}[];`); - } else if (type === 'entity') { - fields.push(` ${f}?: ${md.validations[f].entity};`); - } else { - fields.push(` ${f}?: ${type};`); - } - }); + const refIdList = {}; + + Object.keys(metadata).forEach(function (m) { + const md = metadata[m]; + + const relationshipFields = Object.keys( + md.transformations + ).filter((tranField) => + [ + 'connectOne', + 'connectExclusiveList', + 'connectExclusiveListCreate', + 'connectMany', + ].includes(md.transformations[tranField].name) + ); + + const objectFields = Object.keys(md.transformations).filter( + (tranField) => + ['toString', 'toStringDefaultArray'].includes( + md.transformations[tranField].name + ) && !Object.keys(md.validations || {}).includes(tranField) + ); + + const fields = []; + fields.push(` ${md.refKey}?: string; // Primary Key`); + md.sync + .concat(md.read ? md.read : []) + .filter((s) => !relationshipFields.includes(s)) + .filter((s) => !objectFields.includes(s)) + .filter((s) => s != md.refKey) + .slice() + .forEach((f) => { + if (convertToGatewayId && f == 'namespace') { + f = 'gatewayId'; + } + let type = + md.validations && f in md.validations + ? md.validations[f].type + : 'string'; + if (type === 'enum') { + fields.push( + ` ${f}?: ${md.validations[f].values + .map((v) => `"${v}"`) + .join(' | ')};` + ); + } else if (type === 'entityArray') { + fields.push(` ${f}?: ${md.validations[f].entity}[];`); + } else if (type === 'entity') { + fields.push(` ${f}?: ${md.validations[f].entity};`); + } else { + fields.push(` ${f}?: ${type};`); + } + }); + + objectFields + .map((field) => { + if (convertToGatewayId && field == 'namespace') { + field = 'gatewayId'; + } - objectFields - .map((field) => { - const listToMatch = md.transformations[field].list; - const mdRelField = Object.keys(metadata) - .filter( - (entity) => - metadata[entity].query === listToMatch || entity === listToMatch - ) - .pop(); - if (md.transformations[field].name === 'toStringDefaultArray') { - return ` ${field}?: string[];`; - } else { - return ` ${field}?: any; // ${md.transformations[field].name}`; - } - }) - .forEach((s) => fields.push(s)); - - relationshipFields - .map((field) => { - const listToMatch = md.transformations[field].list; - const mdRelField = Object.keys(metadata) - .filter( - (entity) => - metadata[entity].query === listToMatch || entity === listToMatch - ) - .pop(); - if (md.transformations[field].name === 'connectOne') { - refIdList[mdRelField] = true; - return ` ${field}?: ${mdRelField}RefID;`; - } else if ( - md.transformations[field].name === 'connectExclusiveList' || - md.transformations[field].name === 'connectExclusiveListCreate' - ) { - return ` ${field}?: ${mdRelField}[];`; - } else { - refIdList[mdRelField] = true; - return ` ${field}?: ${mdRelField}RefID[];`; - } - }) - .forEach((s) => fields.push(s)); - - const example = 'example' in md ? commentedExample(md.example) : ' *'; - - content += ` + const listToMatch = md.transformations[field].list; + const mdRelField = Object.keys(metadata) + .filter( + (entity) => + metadata[entity].query === listToMatch || entity === listToMatch + ) + .pop(); + if (md.transformations[field].name === 'toStringDefaultArray') { + return ` ${field}?: string[];`; + } else { + return ` ${field}?: any; // ${md.transformations[field].name}`; + } + }) + .forEach((s) => fields.push(s)); + + relationshipFields + .map((field) => { + if (convertToGatewayId && field == 'namespace') { + field = 'gatewayId'; + } + + const listToMatch = md.transformations[field].list; + const mdRelField = Object.keys(metadata) + .filter( + (entity) => + metadata[entity].query === listToMatch || entity === listToMatch + ) + .pop(); + if (md.transformations[field].name === 'connectOne') { + refIdList[mdRelField] = true; + return ` ${field}?: ${mdRelField}RefID;`; + } else if ( + md.transformations[field].name === 'connectExclusiveList' || + md.transformations[field].name === 'connectExclusiveListCreate' + ) { + return ` ${field}?: ${mdRelField}[];`; + } else { + refIdList[mdRelField] = true; + return ` ${field}?: ${mdRelField}RefID[];`; + } + }) + .forEach((s) => fields.push(s)); + + const example = 'example' in md ? commentedExample(md.example) : ' *'; + + content += ` /** * @tsoaModel @@ -115,20 +127,31 @@ export interface ${m} { ${fields.join('\n')} } `; -}); + }); -Object.keys(refIdList) - .sort() - .forEach((id) => { - content += ` + Object.keys(refIdList) + .sort() + .forEach((id) => { + content += ` /** * @tsoaModel */ export type ${id}RefID = string `; - }); + }); + + return content; +} + +fs.writeFile('controllers/v3/types.ts', buildContent(true), (err) => { + if (err) { + console.error(err); + return; + } + console.log('Updated file: controllers/v3/types.ts'); +}); -fs.writeFile('controllers/v2/types.ts', content, (err) => { +fs.writeFile('controllers/v2/types.ts', buildContent(false), (err) => { if (err) { console.error(err); return; diff --git a/src/tsoa-v3.json b/src/tsoa-v3.json new file mode 100644 index 000000000..10e323533 --- /dev/null +++ b/src/tsoa-v3.json @@ -0,0 +1,73 @@ +{ + "entryFile": "server.ts", + "noImplicitAdditionalProperties": "throw-on-extras", + "controllerPathGlobs": ["controllers/v3/*Controller.ts"], + "spec": { + "name": "APS Directory API", + "version": "3.0.0", + "description": "", + "outputDirectory": "controllers/v3", + "specVersion": 3, + "basePath": "/ds/api/v3", + "specFileBaseName": "openapi", + "yaml": true, + "securityDefinitions": { + "jwt": { + "type": "oauth2", + "description": "Authz Client Credential", + "scheme": "bearer", + "bearerFormat": "JWT", + "flow": "application", + "tokenUrl": "https://token_endpoint" + }, + "portal": { + "type": "http", + "description": "Authz Portal Login", + "scheme": "bearer", + "bearerFormat": "JWT" + }, + "openid": { + "type": "openIdConnect", + "description": "OIDC Login", + "openIdConnectUrl": "https://well_known_endpoint" + } + }, + "tags": [ + { + "name": "API Directory", + "description": "Discover all the great BC Government APIs" + }, + { + "name": "Organizations", + "description": "Manage organizational access control" + }, + { + "name": "Gateways", + "description": "Get aggregated information about gateways" + }, + { + "name": "Gateway Services", + "description": "View your Gateway Service details" + }, + { + "name": "Products", + "description": "Manage your Products and Environments for publishing to the API Directory" + }, + { + "name": "Authorization Profiles", + "description": "Configure the integration to external Identity Providers" + }, + { + "name": "Documentation", + "description": "View public documentation and publish documentation for your APIs" + } + ] + }, + "routes": { + "routesDir": "controllers/v3", + "basePath": "/ds/api/v3", + "iocModule": "controllers/ioc", + "authenticationModule": "./auth/auth-tsoa" + }, + "ignore": ["**/node_modules/**"] +} From a49790906fc57ef11860ab3c811f04dcf57ac929 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Tue, 4 Jun 2024 14:37:16 -0700 Subject: [PATCH 037/191] hide Recently Viewed heading if none --- .../components/namespace-menu/namespace-menu.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index 0171cedf5..0e26411c3 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -182,10 +182,13 @@ const NamespaceMenu: React.FC = ({ ) : ( - - Recently viewed - - ) + recentNamespaces.length > 0 ? + ( + + Recently viewed + + ) : null + ) } > {(search !== '' && namespaceSearchResults.length === 0) && ( From 74774be1b0cbe90484a9a827400be84d101aecb0 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 4 Jun 2024 14:43:20 -0700 Subject: [PATCH 038/191] udp babel config --- src/babel.config.js | 2 +- src/package-lock.json | 368 +++++++++++++++++++++++------------------- src/package.json | 1 + 3 files changed, 207 insertions(+), 164 deletions(-) diff --git a/src/babel.config.js b/src/babel.config.js index 4dc8ce035..fefd5534e 100644 --- a/src/babel.config.js +++ b/src/babel.config.js @@ -4,5 +4,5 @@ module.exports = { '@babel/preset-react', '@babel/preset-typescript', ], - plugins: ['transform-class-properties', 'istanbul'], + plugins: ['@babel/transform-class-properties', 'istanbul'], }; diff --git a/src/package-lock.json b/src/package-lock.json index c4d327133..0edee22b0 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -94,6 +94,7 @@ }, "devDependencies": { "@babel/core": "^7.15.0", + "@babel/plugin-transform-class-properties": "^7.24.6", "@babel/preset-env": "^7.12.11", "@babel/preset-react": "^7.12.10", "@babel/preset-typescript": "^7.12.7", @@ -137,7 +138,6 @@ "eslint-plugin-react": "^7.22.0", "eslint-plugin-react-hooks": "^4.2.0", "identity-obj-proxy": "^3.0.0", - "istanbul-lib-coverage": "^3.2.0", "jest": "^27.5.1", "mocha": "^5.2.0", "msw": "^0.28.2", @@ -1567,11 +1567,12 @@ "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" }, "node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", + "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", "dependencies": { - "@babel/highlight": "^7.16.7" + "@babel/highlight": "^7.24.6", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -1628,11 +1629,11 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.6.tgz", + "integrity": "sha512-DitEzDfOMnd13kZnDqns1ccmftwJTS9DMkyn9pYTxulS7bZxUxpMly3Nf23QQ6NwA4UB8lAqjbqWtyvElEMAkg==", "dependencies": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1668,17 +1669,19 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.9.tgz", - "integrity": "sha512-kUjip3gruz6AJKOq5i3nC6CoCEEF/oHH3cp6tOZhB+IyyyPyW0g1Gfsxn3mkk6S08pIA2y8GQh609v9G/5sHVQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.6.tgz", + "integrity": "sha512-djsosdPJVZE6Vsw3kk7IPRWethP94WHGOhQTc67SNXE0ZzMhHgALw8iGmYS0TD1bbMM0VDROy43od7/hN6WYcA==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-member-expression-to-functions": "^7.17.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/helper-replace-supers": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7" + "@babel/helper-annotate-as-pure": "^7.24.6", + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-function-name": "^7.24.6", + "@babel/helper-member-expression-to-functions": "^7.24.6", + "@babel/helper-optimise-call-expression": "^7.24.6", + "@babel/helper-replace-supers": "^7.24.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -1722,12 +1725,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", - "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", - "dependencies": { - "@babel/types": "^7.16.7" - }, + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", + "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==", "engines": { "node": ">=6.9.0" } @@ -1744,12 +1744,12 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz", + "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==", "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1767,11 +1767,11 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz", - "integrity": "sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.6.tgz", + "integrity": "sha512-OTsCufZTxDUsv2/eDXanw/mUZHWOxSbEmC3pP8cgjcy5rgeVPWWMStnv274DV60JtHxTk0adT0QrCzC4M9NWGg==", "dependencies": { - "@babel/types": "^7.17.0" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1807,20 +1807,20 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", - "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.6.tgz", + "integrity": "sha512-3SFDJRbx7KuPRl8XDUr8O7GAEB8iGyWPjLKJh/ywP/Iy9WOmEfMrsWbaZpvBu2HSYn4KQygIsz0O7m8y10ncMA==", "dependencies": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", - "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz", + "integrity": "sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==", "engines": { "node": ">=6.9.0" } @@ -1839,18 +1839,19 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", - "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.6.tgz", + "integrity": "sha512-mRhfPwDqDpba8o1F8ESxsEkJMQkUF8ZIWrAc0FtWhxnjfextxMWxr22RtFizxxSYLjVHDeMgVsRq8BBZR2ikJQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-member-expression-to-functions": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-member-expression-to-functions": "^7.24.6", + "@babel/helper-optimise-call-expression": "^7.24.6" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-simple-access": { @@ -1865,31 +1866,39 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz", - "integrity": "sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.6.tgz", + "integrity": "sha512-jhbbkK3IUKc4T43WadP96a27oYti9gEf1LdyGSP2rHGH77kwLwfhO7TgwnWvxxQVmke0ImmCSS47vcuxEMGD3Q==", "dependencies": { - "@babel/types": "^7.16.0" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", + "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", "dependencies": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz", + "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", + "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", "engines": { "node": ">=6.9.0" } @@ -1930,22 +1939,23 @@ } }, "node_modules/@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", + "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@babel/helper-validator-identifier": "^7.24.6", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.9.tgz", - "integrity": "sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", + "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==", "bin": { "parser": "bin/babel-parser.js" }, @@ -2574,6 +2584,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.6.tgz", + "integrity": "sha512-j6dZ0Z2Z2slWLR3kt9aOmSIrBvnntWjMDN/TVcMPxhXMLmJVqX605CBRlcGI4b32GMbfifTEsdEjGjiE+j/c3A==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-classes": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz", @@ -3442,13 +3468,13 @@ } }, "node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", + "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==", "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/code-frame": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -3475,11 +3501,12 @@ } }, "node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", + "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", + "@babel/helper-string-parser": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6", "to-fast-properties": "^2.0.0" }, "engines": { @@ -43633,9 +43660,9 @@ "integrity": "sha1-l6VGMxiKURq6ZBn8XB+pG0Z+a+E= sha512-C+6PCOO55NLCfS8uQjUKV/6E5XMuUcfOVsix5m0QqCCCKi495NgeQVNfWtAaD71NKHsdmFCJoXUGfir3qWdr9A==" }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -50428,11 +50455,12 @@ } }, "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", + "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", "requires": { - "@babel/highlight": "^7.16.7" + "@babel/highlight": "^7.24.6", + "picocolors": "^1.0.0" } }, "@babel/compat-data": { @@ -50473,11 +50501,11 @@ } }, "@babel/helper-annotate-as-pure": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", - "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.6.tgz", + "integrity": "sha512-DitEzDfOMnd13kZnDqns1ccmftwJTS9DMkyn9pYTxulS7bZxUxpMly3Nf23QQ6NwA4UB8lAqjbqWtyvElEMAkg==", "requires": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.24.6" } }, "@babel/helper-builder-binary-assignment-operator-visitor": { @@ -50501,17 +50529,19 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.9.tgz", - "integrity": "sha512-kUjip3gruz6AJKOq5i3nC6CoCEEF/oHH3cp6tOZhB+IyyyPyW0g1Gfsxn3mkk6S08pIA2y8GQh609v9G/5sHVQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.6.tgz", + "integrity": "sha512-djsosdPJVZE6Vsw3kk7IPRWethP94WHGOhQTc67SNXE0ZzMhHgALw8iGmYS0TD1bbMM0VDROy43od7/hN6WYcA==", "requires": { - "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-member-expression-to-functions": "^7.17.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/helper-replace-supers": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7" + "@babel/helper-annotate-as-pure": "^7.24.6", + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-function-name": "^7.24.6", + "@babel/helper-member-expression-to-functions": "^7.24.6", + "@babel/helper-optimise-call-expression": "^7.24.6", + "@babel/helper-replace-supers": "^7.24.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "semver": "^6.3.1" } }, "@babel/helper-create-regexp-features-plugin": { @@ -50540,12 +50570,9 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", - "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", - "requires": { - "@babel/types": "^7.16.7" - } + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", + "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==" }, "@babel/helper-explode-assignable-expression": { "version": "7.16.7", @@ -50556,12 +50583,12 @@ } }, "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz", + "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==", "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" } }, "@babel/helper-hoist-variables": { @@ -50573,11 +50600,11 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz", - "integrity": "sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.6.tgz", + "integrity": "sha512-OTsCufZTxDUsv2/eDXanw/mUZHWOxSbEmC3pP8cgjcy5rgeVPWWMStnv274DV60JtHxTk0adT0QrCzC4M9NWGg==", "requires": { - "@babel/types": "^7.17.0" + "@babel/types": "^7.24.6" } }, "@babel/helper-module-imports": { @@ -50604,17 +50631,17 @@ } }, "@babel/helper-optimise-call-expression": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", - "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.6.tgz", + "integrity": "sha512-3SFDJRbx7KuPRl8XDUr8O7GAEB8iGyWPjLKJh/ywP/Iy9WOmEfMrsWbaZpvBu2HSYn4KQygIsz0O7m8y10ncMA==", "requires": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.24.6" } }, "@babel/helper-plugin-utils": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", - "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==" + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz", + "integrity": "sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==" }, "@babel/helper-remap-async-to-generator": { "version": "7.16.8", @@ -50627,15 +50654,13 @@ } }, "@babel/helper-replace-supers": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", - "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.6.tgz", + "integrity": "sha512-mRhfPwDqDpba8o1F8ESxsEkJMQkUF8ZIWrAc0FtWhxnjfextxMWxr22RtFizxxSYLjVHDeMgVsRq8BBZR2ikJQ==", "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-member-expression-to-functions": "^7.16.7", - "@babel/helper-optimise-call-expression": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-member-expression-to-functions": "^7.24.6", + "@babel/helper-optimise-call-expression": "^7.24.6" } }, "@babel/helper-simple-access": { @@ -50647,25 +50672,30 @@ } }, "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz", - "integrity": "sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.6.tgz", + "integrity": "sha512-jhbbkK3IUKc4T43WadP96a27oYti9gEf1LdyGSP2rHGH77kwLwfhO7TgwnWvxxQVmke0ImmCSS47vcuxEMGD3Q==", "requires": { - "@babel/types": "^7.16.0" + "@babel/types": "^7.24.6" } }, "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", + "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", "requires": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.24.6" } }, + "@babel/helper-string-parser": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz", + "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==" + }, "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==" + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", + "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==" }, "@babel/helper-validator-option": { "version": "7.16.7", @@ -50694,19 +50724,20 @@ } }, "@babel/highlight": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", - "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", + "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@babel/helper-validator-identifier": "^7.24.6", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" } }, "@babel/parser": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.9.tgz", - "integrity": "sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg==" + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", + "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.16.7", @@ -51104,6 +51135,16 @@ "@babel/helper-plugin-utils": "^7.16.7" } }, + "@babel/plugin-transform-class-properties": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.6.tgz", + "integrity": "sha512-j6dZ0Z2Z2slWLR3kt9aOmSIrBvnntWjMDN/TVcMPxhXMLmJVqX605CBRlcGI4b32GMbfifTEsdEjGjiE+j/c3A==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" + } + }, "@babel/plugin-transform-classes": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz", @@ -51691,13 +51732,13 @@ } }, "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", + "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==", "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/code-frame": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/types": "^7.24.6" } }, "@babel/traverse": { @@ -51718,11 +51759,12 @@ } }, "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", + "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", "requires": { - "@babel/helper-validator-identifier": "^7.16.7", + "@babel/helper-string-parser": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6", "to-fast-properties": "^2.0.0" } }, @@ -82997,9 +83039,9 @@ "integrity": "sha1-l6VGMxiKURq6ZBn8XB+pG0Z+a+E= sha512-C+6PCOO55NLCfS8uQjUKV/6E5XMuUcfOVsix5m0QqCCCKi495NgeQVNfWtAaD71NKHsdmFCJoXUGfir3qWdr9A==" }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" }, "semver-diff": { "version": "3.1.1", diff --git a/src/package.json b/src/package.json index 147f8aad3..e89cb0ca7 100644 --- a/src/package.json +++ b/src/package.json @@ -144,6 +144,7 @@ }, "devDependencies": { "@babel/core": "^7.15.0", + "@babel/plugin-transform-class-properties": "^7.24.6", "@babel/preset-env": "^7.12.11", "@babel/preset-react": "^7.12.10", "@babel/preset-typescript": "^7.12.7", From 422c566bab85aeed629fa80373f742ae89bb3edc Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 4 Jun 2024 15:08:42 -0700 Subject: [PATCH 039/191] adj to run sonarscan --- src/babel.config.js | 2 +- src/package-lock.json | 27 --------------------------- src/package.json | 1 - 3 files changed, 1 insertion(+), 29 deletions(-) diff --git a/src/babel.config.js b/src/babel.config.js index fefd5534e..be4a973b2 100644 --- a/src/babel.config.js +++ b/src/babel.config.js @@ -4,5 +4,5 @@ module.exports = { '@babel/preset-react', '@babel/preset-typescript', ], - plugins: ['@babel/transform-class-properties', 'istanbul'], + // plugins: ['transform-class-properties', 'istanbul'], }; diff --git a/src/package-lock.json b/src/package-lock.json index 0edee22b0..e452672db 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -94,7 +94,6 @@ }, "devDependencies": { "@babel/core": "^7.15.0", - "@babel/plugin-transform-class-properties": "^7.24.6", "@babel/preset-env": "^7.12.11", "@babel/preset-react": "^7.12.10", "@babel/preset-typescript": "^7.12.7", @@ -2584,22 +2583,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.6.tgz", - "integrity": "sha512-j6dZ0Z2Z2slWLR3kt9aOmSIrBvnntWjMDN/TVcMPxhXMLmJVqX605CBRlcGI4b32GMbfifTEsdEjGjiE+j/c3A==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.6", - "@babel/helper-plugin-utils": "^7.24.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-classes": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz", @@ -51135,16 +51118,6 @@ "@babel/helper-plugin-utils": "^7.16.7" } }, - "@babel/plugin-transform-class-properties": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.6.tgz", - "integrity": "sha512-j6dZ0Z2Z2slWLR3kt9aOmSIrBvnntWjMDN/TVcMPxhXMLmJVqX605CBRlcGI4b32GMbfifTEsdEjGjiE+j/c3A==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.24.6", - "@babel/helper-plugin-utils": "^7.24.6" - } - }, "@babel/plugin-transform-classes": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz", diff --git a/src/package.json b/src/package.json index e89cb0ca7..147f8aad3 100644 --- a/src/package.json +++ b/src/package.json @@ -144,7 +144,6 @@ }, "devDependencies": { "@babel/core": "^7.15.0", - "@babel/plugin-transform-class-properties": "^7.24.6", "@babel/preset-env": "^7.12.11", "@babel/preset-react": "^7.12.10", "@babel/preset-typescript": "^7.12.7", From 6d6b48dcf4c431745fd46ab448b44669278293d2 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 4 Jun 2024 15:14:40 -0700 Subject: [PATCH 040/191] default to v3 of api --- .github/workflows/ci-build-deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build-deploy.yaml b/.github/workflows/ci-build-deploy.yaml index a4abaa718..4f9aa28a8 100644 --- a/.github/workflows/ci-build-deploy.yaml +++ b/.github/workflows/ci-build-deploy.yaml @@ -262,7 +262,7 @@ jobs: NEXT_PUBLIC_HELP_ISSUE_URL: value: 'https://github.com/bcgov/api-services-portal/issues' NEXT_PUBLIC_HELP_API_DOCS_URL: - value: '/ds/api/v2/console/' + value: '/ds/api/v3/console/' NEXT_PUBLIC_HELP_SUPPORT_URL: value: 'https://developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/' NEXT_PUBLIC_HELP_RELEASE_URL: From d1dc8c51aad2a7cd3bc4334b2cf46db6a1710067 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Wed, 5 Jun 2024 12:01:50 -0700 Subject: [PATCH 041/191] search both name and displayName --- src/nextapp/components/namespace-menu/namespace-menu.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index 0e26411c3..7f63aa904 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -60,13 +60,12 @@ const NamespaceMenu: React.FC = ({ }) .slice(0, 5); const namespaceSearchResults = React.useMemo(() => { - const result = - data?.allNamespaces ?? []; + const result = data?.allNamespaces ?? []; if (search.trim()) { const regex = new RegExp( - search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') + search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'), 'i' ); - return result.filter((s) => s.name.search(regex) >= 0); + return result.filter((s) => regex.test(s.name) || regex.test(s.displayName)); } return result; }, [data, search]); From 7d83ab83c8ee792ae2a143f68bb88610a7058bf6 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Wed, 5 Jun 2024 12:37:29 -0700 Subject: [PATCH 042/191] change Your Products header --- .../components/page-header/page-header.tsx | 4 +- .../devportal/api-directory/your-products.tsx | 43 ++++++++++++++++--- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/nextapp/components/page-header/page-header.tsx b/src/nextapp/components/page-header/page-header.tsx index 57e097578..163a2b236 100644 --- a/src/nextapp/components/page-header/page-header.tsx +++ b/src/nextapp/components/page-header/page-header.tsx @@ -15,6 +15,7 @@ interface PageHeaderProps { breadcrumb?: { href?: string; text: string }[]; children?: React.ReactNode; title: React.ReactNode; + apiDirectoryNav?: boolean; } const PageHeader: React.FC = ({ @@ -22,9 +23,10 @@ const PageHeader: React.FC = ({ breadcrumb, children, title, + apiDirectoryNav }) => { return ( - + {breadcrumb && ( diff --git a/src/nextapp/pages/devportal/api-directory/your-products.tsx b/src/nextapp/pages/devportal/api-directory/your-products.tsx index ee5954f55..a30595004 100644 --- a/src/nextapp/pages/devportal/api-directory/your-products.tsx +++ b/src/nextapp/pages/devportal/api-directory/your-products.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import ApiDirectoryNav from '@/components/api-directory-nav'; -import { Box, Button, Container, Text } from '@chakra-ui/react'; +import { Box, Button, Container, Flex, Icon, Skeleton, Text, Tooltip } from '@chakra-ui/react'; import EmptyPane from '@/components/empty-pane'; +import { FaCheckCircle } from 'react-icons/fa'; import Head from 'next/head'; import PageHeader from '@/components/page-header'; import { restApi } from '@/shared/services/api'; @@ -25,6 +26,38 @@ const ApiDiscoveryPage: React.FC = () => { ) ); const namespace = useCurrentNamespace(); + const hasNamespace = !!user?.namespace; + const title = ( + <> + {(namespace.isFetching || namespace.isLoading) && ( + + )} + {namespace.isSuccess && !namespace.isFetching && ( + <> + + {namespace.data?.currentNamespace?.displayName} + {namespace.data?.currentNamespace?.orgEnabled && ( + + + + + + )} + + + {namespace?.data.currentNamespace?.name} + + + )} + + ) return ( <> @@ -34,16 +67,14 @@ const ApiDiscoveryPage: React.FC = () => { - + {user && - 'A list of the published and unpublished products under your namespace'} + 'A list of the published and unpublished products under your gateway.'} {!user && 'You must be signed in to view this page'} - + {data?.length === 0 && ( Date: Wed, 5 Jun 2024 13:36:38 -0700 Subject: [PATCH 043/191] add cypress tests for v3 of api --- e2e/cypress.config.ts | 5 +- e2e/cypress/fixtures/api-v3.json | 6 + e2e/cypress/support/auth-commands.ts | 651 +++++++------- e2e/cypress/support/global.d.ts | 108 ++- ...10-jwt-genkp-access-approve-api-rqst.cy.ts | 5 +- ...t-publlicKey-access-approve-api-rqst.cy.ts | 81 +- e2e/cypress/tests/19-api-v3/api-suite.ts | 795 ++++++++++++++++++ e2e/package-lock.json | 266 +++--- e2e/package.json | 1 + local/keycloak/master-realm.json | 6 +- src/auth/auth-tsoa.ts | 7 +- src/batch/data-rules.js | 2 +- src/batch/feed-worker.ts | 11 + src/controllers/ioc/keystoneInjector.ts | 2 +- src/controllers/v3/GatewayController.ts | 13 +- src/controllers/v3/IdentifierController.ts | 5 +- src/controllers/v3/OrganizationController.ts | 38 +- src/controllers/v3/ProductController.ts | 4 +- src/controllers/v3/openapi.yaml | 94 ++- src/controllers/v3/routes.ts | 76 +- src/services/identifiers.ts | 2 + src/services/keycloak/group-service.ts | 18 + src/services/org-groups/namespace.ts | 4 +- src/test/services/batch/batch-utils.test.js | 31 + src/tsoa-v3.json | 4 - 25 files changed, 1708 insertions(+), 527 deletions(-) create mode 100644 e2e/cypress/fixtures/api-v3.json create mode 100644 e2e/cypress/tests/19-api-v3/api-suite.ts diff --git a/e2e/cypress.config.ts b/e2e/cypress.config.ts index 2cf4bc2a3..1041093a2 100644 --- a/e2e/cypress.config.ts +++ b/e2e/cypress.config.ts @@ -38,6 +38,7 @@ export default defineConfig({ './cypress/tests/16-*/*.ts', './cypress/tests/17-*/*.ts', './cypress/tests/18-*/*.ts', + './cypress/tests/19-*/*.ts', ] return config }, @@ -59,7 +60,7 @@ export default defineConfig({ env: { CLIENT_ID: 'aps-portal', CLIENT_SECRET: '8e1a17ed-cb93-4806-ac32-e303d1c86018', - OIDC_ISSUER: 'http://keycloak.localtest.me:9081', + OIDC_ISSUER: 'http://keycloak.localtest.me:9081/auth/realms/master', TOKEN_URL: 'http://keycloak.localtest.me:9081/auth/realms/master/protocol/openid-connect/token', GWA_API_URL: 'http://gwa-api.localtest.me:2000/v2', @@ -69,6 +70,8 @@ export default defineConfig({ BASE_URL: 'http://oauth2proxy.localtest.me:4180', KEYCLOAK_URL: 'http://keycloak.localtest.me:9081', WEBAPP_URL: 'http://html-sample-app.localtest.me:4242', + DEV_USERNAME: 'janis@idir', + DEV_PASSWORD: 'awsummer', }, retries: { runMode: 2, diff --git a/e2e/cypress/fixtures/api-v3.json b/e2e/cypress/fixtures/api-v3.json new file mode 100644 index 000000000..9ecb8f180 --- /dev/null +++ b/e2e/cypress/fixtures/api-v3.json @@ -0,0 +1,6 @@ +{ + "gateway": { + "displayName": "My Gateway" + }, + "model": [] +} diff --git a/e2e/cypress/support/auth-commands.ts b/e2e/cypress/support/auth-commands.ts index fd289af65..5256a563f 100644 --- a/e2e/cypress/support/auth-commands.ts +++ b/e2e/cypress/support/auth-commands.ts @@ -7,15 +7,15 @@ import { checkElementExists } from './e2e' // import _ = require('cypress/types/lodash') const njwt = require('njwt') -const fs = require('fs'); +const fs = require('fs') const config = require('../fixtures/manage-control/kong-plugin-config.json') const jose = require('node-jose') -const YAML = require('yamljs'); +const YAML = require('yamljs') -const forge = require('node-forge'); +const forge = require('node-forge') let headers: any @@ -81,10 +81,16 @@ Cypress.Commands.add('keycloakLogin', (username: string, password: string) => { Cypress.Commands.add('getLastConsumerID', () => { let id: any - cy.get('[data-testid="all-consumer-control-tbl"]').find('tr').last().find('td').first().find('a').then(($text) => { - id = $text.text() - return id - }) + cy.get('[data-testid="all-consumer-control-tbl"]') + .find('tr') + .last() + .find('td') + .first() + .find('a') + .then(($text) => { + id = $text.text() + return id + }) }) Cypress.Commands.add('resetCredential', (accessRole: string) => { @@ -111,31 +117,34 @@ Cypress.Commands.add('resetCredential', (accessRole: string) => { }) }) -Cypress.Commands.add('getUserSessionTokenValue', (namespace: string, isNamespaceSelected?: true) => { - const login = new LoginPage() - const home = new HomePage() - const na = new NamespaceAccessPage() - let userSession: string - cy.visit('/') - cy.reload() - cy.fixture('apiowner').as('apiowner') - cy.preserveCookies() - cy.visit(login.path) - cy.getUserSession().then(() => { - cy.get('@apiowner').then(({ user }: any) => { - cy.login(user.credentials.username, user.credentials.password) - cy.log('Logged in!') - // home.useNamespace(apiTest.namespace) - if (isNamespaceSelected || undefined) { - home.useNamespace(namespace) - } - cy.get('@login').then(function (xhr: any) { - userSession = xhr.response.headers['x-auth-request-access-token'] - return userSession +Cypress.Commands.add( + 'getUserSessionTokenValue', + (namespace: string, isNamespaceSelected?: true) => { + const login = new LoginPage() + const home = new HomePage() + const na = new NamespaceAccessPage() + let userSession: string + cy.visit('/') + cy.reload() + cy.fixture('apiowner').as('apiowner') + cy.preserveCookies() + cy.visit(login.path) + cy.getUserSession().then(() => { + cy.get('@apiowner').then(({ user }: any) => { + cy.login(user.credentials.username, user.credentials.password) + cy.log('Logged in!') + // home.useNamespace(apiTest.namespace) + if (isNamespaceSelected || undefined) { + home.useNamespace(namespace) + } + cy.get('@login').then(function (xhr: any) { + userSession = xhr.response.headers['x-auth-request-access-token'] + return userSession + }) }) }) - }) -}) + } +) Cypress.Commands.add('getUserSessionResponse', () => { cy.getUserSession().then(() => { @@ -188,14 +197,13 @@ Cypress.Commands.add('loginByAuthAPI', (username: string, password: string) => { ...user, }, } - cy.log(JSON.stringify(userItem)) + cy.wrap(userItem).as('loginByAuthApiResponse') }) log.snapshot('after') log.end() }) Cypress.Commands.add('logout', () => { - cy.log('< Logging out') cy.getSession().then(() => { cy.get('@session').then((res: any) => { @@ -209,7 +217,6 @@ Cypress.Commands.add('logout', () => { }) Cypress.Commands.add('keycloakLogout', () => { - cy.log('< Logging out') cy.get('.dropdown-toggle.ng-binding').click() cy.contains('Sign Out').click() @@ -228,7 +235,7 @@ Cypress.Commands.add('getAccessToken', (client_id: string, client_secret: string client_secret, }, form: true, - failOnStatusCode: false + failOnStatusCode: false, }).then((res) => { cy.wrap(res).as('accessTokenResponse') // expect(res.status).to.eq(200) @@ -244,47 +251,62 @@ Cypress.Commands.add('getServiceOrRouteID', (configType: string, host: string) = }).then((res) => { expect(res.status).to.eq(200) if (config === 'routes') { - cy.saveState(config + 'ID', Cypress._.get((Cypress._.filter(res.body.data, ["hosts", [host + ".api.gov.bc.ca"]]))[0], 'id')) - } - else { - cy.saveState(config + 'ID', Cypress._.get((Cypress._.filter(res.body.data, ["name", host]))[0], 'id')) + cy.saveState( + config + 'ID', + Cypress._.get( + Cypress._.filter(res.body.data, ['hosts', [host + '.api.gov.bc.ca']])[0], + 'id' + ) + ) + } else { + cy.saveState( + config + 'ID', + Cypress._.get(Cypress._.filter(res.body.data, ['name', host])[0], 'id') + ) } }) }) -Cypress.Commands.add('publishApi', (fileNames: any, namespace: string, flag?: boolean) => { - let fixtureFile = flag ? "state/regen" : "state/store"; - cy.log('< Publish API') - let fileName = '' - if (fileNames instanceof Array) { - for (const filepath of fileNames) { - fileName = fileName + ' ./cypress/fixtures/' + filepath; +Cypress.Commands.add( + 'publishApi', + (fileNames: any, namespace: string, flag?: boolean) => { + let fixtureFile = flag ? 'state/regen' : 'state/store' + cy.log('< Publish API') + let fileName = '' + if (fileNames instanceof Array) { + for (const filepath of fileNames) { + fileName = fileName + ' ./cypress/fixtures/' + filepath + } + } else { + fileName = ' ./cypress/fixtures/' + fileNames } - } - else { - fileName = ' ./cypress/fixtures/' + fileNames - } - const requestName: string = 'publishAPI' - cy.fixture(fixtureFile).then((creds: any) => { - const serviceAcctCreds = JSON.parse(creds.credentials) - cy.getAccessToken(serviceAcctCreds.clientId, serviceAcctCreds.clientSecret).then( - () => { - cy.wait(3000) - cy.get('@accessTokenResponse').then((res: any) => { - cy.executeCliCommand('gwa config set --namespace ' + namespace).then((response) => { - cy.executeCliCommand('gwa config set --token ' + res.body.access_token).then((response) => { - { - expect(response.stdout).to.contain("Config settings saved") - cy.executeCliCommand('gwa pg ' + fileName).then((response) => { - expect(response.stdout).to.contain("Gateway config published") + const requestName: string = 'publishAPI' + cy.fixture(fixtureFile).then((creds: any) => { + const serviceAcctCreds = JSON.parse(creds.credentials) + cy.getAccessToken(serviceAcctCreds.clientId, serviceAcctCreds.clientSecret).then( + () => { + cy.wait(3000) + cy.get('@accessTokenResponse').then((res: any) => { + cy.executeCliCommand('gwa config set --namespace ' + namespace).then( + (response) => { + cy.executeCliCommand( + 'gwa config set --token ' + res.body.access_token + ).then((response) => { + { + expect(response.stdout).to.contain('Config settings saved') + cy.executeCliCommand('gwa pg ' + fileName).then((response) => { + expect(response.stdout).to.contain('Gateway config published') + }) + } }) } - }) + ) }) - }) - }) - }) -}) + } + ) + }) + } +) Cypress.Commands.add('deleteAllCookies', () => { cy.clearCookies() @@ -293,120 +315,133 @@ Cypress.Commands.add('deleteAllCookies', () => { cy.clearCookie('keystone.sid') cy.clearCookie('_oauth2_proxy') cy.exec('npm cache clear --force') - var cookies = document.cookie.split(";"); + var cookies = document.cookie.split(';') for (var i = 0; i < cookies.length; i++) { - var cookie = cookies[i]; - var eqPos = cookie.indexOf("="); - var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; - document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; + var cookie = cookies[i] + var eqPos = cookie.indexOf('=') + var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie + document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT' } }) -Cypress.Commands.add('makeKongRequest', (serviceName: string, methodType: string, key?: string) => { - let authorization - cy.fixture('state/regen').then((creds: any) => { - cy.wait(5000) - let token = key - if (key == undefined) { - token = creds.apikey +Cypress.Commands.add( + 'makeKongRequest', + (serviceName: string, methodType: string, key?: string) => { + let authorization + cy.fixture('state/regen').then((creds: any) => { + cy.wait(5000) + let token = key + if (key == undefined) { + token = creds.apikey + } + const service = serviceName + cy.log('Token->' + token) + return cy.request({ + url: Cypress.env('KONG_URL'), + method: methodType, + headers: { 'x-api-key': `${token}`, Host: `${service}` + '.api.gov.bc.ca' }, + failOnStatusCode: false, + auth: { + bearer: token, + }, + }) + }) + } +) + +Cypress.Commands.add( + 'makeKongGatewayRequest', + (endpoint: string, requestName: string, methodType: string) => { + let body = {} + var serviceEndPoint = endpoint + body = config[requestName] + if (requestName == '') { + body = {} } - const service = serviceName - cy.log("Token->" + token) return cy.request({ - url: Cypress.env('KONG_URL'), + url: Cypress.env('KONG_CONFIG_URL') + '/' + serviceEndPoint, method: methodType, - headers: { 'x-api-key': `${token}`, 'Host': `${service}` + '.api.gov.bc.ca' }, + body: body, + form: true, failOnStatusCode: false, - auth: { - bearer: token, - } }) - }) -}) - -Cypress.Commands.add('makeKongGatewayRequest', (endpoint: string, requestName: string, methodType: string) => { - let body = {} - var serviceEndPoint = endpoint - body = config[requestName] - if (requestName == '') { - body = {} } - return cy.request({ - url: Cypress.env('KONG_CONFIG_URL') + '/' + serviceEndPoint, - method: methodType, - body: body, - form: true, - failOnStatusCode: false - }) -}) - -Cypress.Commands.add('makeKongGatewayRequestUsingClientIDSecret', (hostURL: string, methodType = 'GET') => { - cy.readFile('cypress/fixtures/state/store.json').then((store_res) => { - - let cc = JSON.parse(store_res.clientidsecret) - - cy.getAccessToken(cc.clientId, cc.clientSecret).then(() => { - cy.get('@accessTokenResponse').then((token_res: any) => { - let token = token_res.body.access_token - cy.request({ - method: methodType, - url: Cypress.env('KONG_URL'), - failOnStatusCode: false, - headers: { - Host: hostURL, - }, - auth: { - bearer: token, - }, +) + +Cypress.Commands.add( + 'makeKongGatewayRequestUsingClientIDSecret', + (hostURL: string, methodType = 'GET') => { + cy.readFile('cypress/fixtures/state/store.json').then((store_res) => { + let cc = JSON.parse(store_res.clientidsecret) + + cy.getAccessToken(cc.clientId, cc.clientSecret).then(() => { + cy.get('@accessTokenResponse').then((token_res: any) => { + let token = token_res.body.access_token + cy.request({ + method: methodType, + url: Cypress.env('KONG_URL'), + failOnStatusCode: false, + headers: { + Host: hostURL, + }, + auth: { + bearer: token, + }, + }) }) }) }) - }) -}) - -Cypress.Commands.add('updateKongPlugin', (pluginName: string, name: string, endPoint?: string, verb = 'POST') => { - cy.fixture('state/store').then((creds: any) => { - let body = {} - const pluginID = pluginName.toLowerCase() + 'id' - const id = creds[pluginID] - let endpoint - if (pluginName == '') - endpoint = 'plugins' - else if (id !== undefined) - endpoint = pluginName.toLowerCase() + '/' + id.toString() + '/' + 'plugins' - endpoint = (typeof endPoint !== 'undefined') ? endPoint : endpoint - body = config[name] - cy.log("Body->" + body) - return cy.request({ - url: Cypress.env('KONG_CONFIG_URL') + '/' + endpoint, - method: verb, - body: body, - form: true, - failOnStatusCode: false + } +) + +Cypress.Commands.add( + 'updateKongPlugin', + (pluginName: string, name: string, endPoint?: string, verb = 'POST') => { + cy.fixture('state/store').then((creds: any) => { + let body = {} + const pluginID = pluginName.toLowerCase() + 'id' + const id = creds[pluginID] + let endpoint + if (pluginName == '') endpoint = 'plugins' + else if (id !== undefined) + endpoint = pluginName.toLowerCase() + '/' + id.toString() + '/' + 'plugins' + endpoint = typeof endPoint !== 'undefined' ? endPoint : endpoint + body = config[name] + cy.log('Body->' + body) + return cy.request({ + url: Cypress.env('KONG_CONFIG_URL') + '/' + endpoint, + method: verb, + body: body, + form: true, + failOnStatusCode: false, + }) }) - }) -}) - -Cypress.Commands.add('updateKongPluginForJSONRequest', (jsonBody: string, endPoint: string, verb = 'POST') => { - cy.fixture('state/store').then((creds: any) => { - let body = {} - let headers = { "content-type": "application/json", "accept": "application/json" } - body = jsonBody - return cy.request({ - url: Cypress.env('KONG_CONFIG_URL') + '/' + endPoint, - method: verb, - body: body, - headers: headers, - failOnStatusCode: false + } +) + +Cypress.Commands.add( + 'updateKongPluginForJSONRequest', + (jsonBody: string, endPoint: string, verb = 'POST') => { + cy.fixture('state/store').then((creds: any) => { + let body = {} + let headers = { 'content-type': 'application/json', accept: 'application/json' } + body = jsonBody + return cy.request({ + url: Cypress.env('KONG_CONFIG_URL') + '/' + endPoint, + method: verb, + body: body, + headers: headers, + failOnStatusCode: false, + }) }) - }) -}) + } +) -Cypress.Commands.add("generateKeystore", async () => { +Cypress.Commands.add('generateKeystore', async () => { let keyStore = jose.JWK.createKeyStore() await keyStore.generate('RSA', 2048, { alg: 'RS256', use: 'sig' }) return JSON.stringify(keyStore.toJSON(true), null, ' ') -}); +}) Cypress.Commands.add('setHeaders', (headerValues: any) => { headers = headerValues @@ -417,31 +452,58 @@ Cypress.Commands.add('setRequestBody', (body: any) => { }) Cypress.Commands.add('setAuthorizationToken', (token: string) => { - headers["Authorization"] = "Bearer " + token - headers = headers + headers['Authorization'] = 'Bearer ' + token +}) + +Cypress.Commands.add('callAPI', (endPoint: string, methodType: string) => { + let body = '{}' + let requestData: any = {} + if (methodType.toUpperCase() === 'PUT' || methodType.toUpperCase() === 'POST') { + body = requestBody + } + + requestData['appname'] = 'Test1' + requestData['url'] = Cypress.env('BASE_URL') + '/' + endPoint + requestData['headers'] = headers + requestData['body'] = '' + requestData['method'] = methodType + + cy.request({ + url: Cypress.env('BASE_URL') + '/' + endPoint, + method: methodType, + body, + headers, + failOnStatusCode: false, + }).then((apiResponse) => { + // You can also return data or use it in further tests + const responseData = { + apiRes: apiResponse, + } + // cy.addToAstraScanIdList(response2.body.status) + return responseData + }) }) Cypress.Commands.add('makeAPIRequest', (endPoint: string, methodType: string) => { - let body = {} let requestData: any = {} if (methodType.toUpperCase() === 'PUT' || methodType.toUpperCase() === 'POST') { body = requestBody } - requestData['appname'] = "Test1" + requestData['appname'] = 'Test1' requestData['url'] = Cypress.env('BASE_URL') + '/' + endPoint requestData['headers'] = headers - requestData['body'] = "" + requestData['body'] = '' requestData['method'] = methodType - + // Scan request with Astra cy.request({ url: 'http://astra.localtest.me:8094/scan/', method: 'POST', body: requestData, headers: headers, - failOnStatusCode: false + failOnStatusCode: false, }).then((astraResponse) => { // Actual API request cy.request({ @@ -449,24 +511,24 @@ Cypress.Commands.add('makeAPIRequest', (endPoint: string, methodType: string) => method: methodType, body: body, headers: headers, - failOnStatusCode: false + failOnStatusCode: false, }).then((apiResponse) => { // You can also return data or use it in further tests const responseData = { astraRes: astraResponse, apiRes: apiResponse, - }; + } // cy.addToAstraScanIdList(response2.body.status) - return responseData; + return responseData }) - }); + }) }) Cypress.Commands.add('makeAPIRequestForScanResult', (scanID: string) => { return cy.request({ - url: 'http://astra.localtest.me:8094/alerts/' + scanID, + url: 'http://astra.localtest.me:8094/alerts/' + scanID, method: 'GET', - failOnStatusCode: false + failOnStatusCode: false, }) }) @@ -478,151 +540,168 @@ Cypress.Commands.add('selectLoginOptions', (username: string) => { cy.get(login.loginDropDown).click() if (username.includes('idir')) { login.selectAPIProviderLoginOption() - } - else { + } else { login.selectDeveloperLoginOption() } }) Cypress.Commands.add('verifyToastMessage', (msg: string) => { - cy.get('[role="alert"]', { timeout: 2000 }).closest('div').invoke('text') + cy.get('[role="alert"]', { timeout: 2000 }) + .closest('div') + .invoke('text') .then((text) => { - const toastText = text; - expect(toastText).to.contain(msg); + const toastText = text + expect(toastText).to.contain(msg) }) }) -Cypress.Commands.add('compareJSONObjects', (actualResponse: any, expectedResponse: any, indexFlag = false) => { - let response = actualResponse - if (indexFlag) { - const index = actualResponse.findIndex((x: { name: string }) => x.name === expectedResponse.name); - response = actualResponse[index] - } - for (var p in expectedResponse) { - if (typeof (expectedResponse[p]) === "object") { - var objectValue1 = expectedResponse[p], - objectValue2 = response[p]; - for (var value in objectValue1) { - if (!(['activityAt', 'id'].includes(value))) { - cy.compareJSONObjects(objectValue2[value], objectValue1[value]); +Cypress.Commands.add( + 'compareJSONObjects', + (actualResponse: any, expectedResponse: any, indexFlag = false) => { + let response = actualResponse + if (indexFlag) { + const index = actualResponse.findIndex( + (x: { name: string }) => x.name === expectedResponse.name + ) + response = actualResponse[index] + } + for (var p in expectedResponse) { + if (typeof expectedResponse[p] === 'object') { + var objectValue1 = expectedResponse[p], + objectValue2 = response[p] + for (var value in objectValue1) { + if (!['activityAt', 'id'].includes(value)) { + cy.compareJSONObjects(objectValue2[value], objectValue1[value]) + } + } + } else { + if (expectedResponse[p] == 'true' || expectedResponse[p] == 'false') + Boolean(expectedResponse[p]) + if (['organization', 'organizationUnit'].includes(p) && !indexFlag) { + response[p] = response[p]['name'] + } + if ( + response[p] !== expectedResponse[p] && + !['clientSecret', 'appId', 'isInCatalog', 'isDraft', 'consumer', 'id'].includes( + p + ) + ) { + cy.log('Different Value ->' + expectedResponse[p]) + assert.fail('JSON value mismatch for ' + p) } - } - } else { - if ((expectedResponse[p] == 'true') || (expectedResponse[p] == 'false')) - Boolean(expectedResponse[p]) - if (['organization', 'organizationUnit'].includes(p) && (!indexFlag)) { - response[p] = response[p]['name'] - } - if ((response[p] !== expectedResponse[p]) && !(['clientSecret', 'appId', 'isInCatalog', 'isDraft', 'consumer', 'id'].includes(p))) { - cy.log("Different Value ->" + expectedResponse[p]) - assert.fail("JSON value mismatch for " + p) } } } -}) +) + +Cypress.Commands.add( + 'updatePluginFile', + (filename: string, serviceName: string, pluginFileName: string) => { + cy.readFile('cypress/fixtures/' + pluginFileName).then(($el) => { + let newObj: any + newObj = YAML.parse($el) + cy.readFile('cypress/fixtures/' + filename).then((content: any) => { + let obj = YAML.parse(content) + const keys = Object.keys(obj) + Object.keys(obj.services).forEach(function (key, index) { + if (obj.services[index].name == serviceName) { + obj.services[index].plugins = newObj.plugins + } + }) + const yamlString = YAML.stringify(obj, 'utf8') + cy.writeFile('cypress/fixtures/' + filename, yamlString) + }) + }) + } +) -Cypress.Commands.add('updatePluginFile', (filename: string, serviceName: string, pluginFileName: string) => { - cy.readFile('cypress/fixtures/' + pluginFileName).then(($el) => { - let newObj: any - newObj = YAML.parse($el) +Cypress.Commands.add( + 'updatePropertiesOfPluginFile', + (filename: string, propertyName: any, propertyValue: any) => { cy.readFile('cypress/fixtures/' + filename).then((content: any) => { let obj = YAML.parse(content) - const keys = Object.keys(obj); - Object.keys(obj.services).forEach(function (key, index) { - if (obj.services[index].name == serviceName) { - obj.services[index].plugins = newObj.plugins - } - }); - const yamlString = YAML.stringify(obj, 'utf8'); + const keys = Object.keys(obj) + if (propertyName === 'config.anonymous') { + obj.plugins[0].config.anonymous = propertyValue + } else if (propertyName === 'tags') { + obj.plugins[0][propertyName] = propertyValue + } else { + Object.keys(obj.services).forEach(function (key, index) { + if (propertyName == 'methods') { + obj.services[0].routes[0].methods = propertyValue + } else { + obj.services[0].plugins[0].config[propertyName] = propertyValue + } + }) + } + const yamlString = YAML.stringify(obj, 'utf8') cy.writeFile('cypress/fixtures/' + filename, yamlString) }) - }) -}) + } +) +Cypress.Commands.add( + 'getTokenUsingJWKCredentials', + (credential: any, privateKey: any) => { + let jwkCred = JSON.parse(credential) + let clientId = jwkCred.clientId + let tokenEndpoint = jwkCred.tokenEndpoint + let now = Math.floor(new Date().getTime() / 1000) + let plus5Minutes = new Date((now + 5 * 60) * 1000) + let alg = 'RS256' -Cypress.Commands.add('updatePropertiesOfPluginFile', (filename: string, propertyName: any, propertyValue: any) => { - cy.readFile('cypress/fixtures/' + filename).then((content: any) => { - let obj = YAML.parse(content) - const keys = Object.keys(obj); - if (propertyName === "config.anonymous") { - obj.plugins[0].config.anonymous = propertyValue + let claims = { + aud: Cypress.env('OIDC_ISSUER'), } - else if (propertyName === "tags") { - obj.plugins[0][propertyName] = propertyValue - } - else { - Object.keys(obj.services).forEach(function (key, index) { - if (propertyName == "methods") { - obj.services[0].routes[0].methods = propertyValue - } - else { - obj.services[0].plugins[0].config[propertyName] = propertyValue - } - }); - } - const yamlString = YAML.stringify(obj, 'utf8'); - cy.writeFile('cypress/fixtures/' + filename, yamlString) - }) -}) - -Cypress.Commands.add('getTokenUsingJWKCredentials', (credential: any, privateKey: any) => { - let jwkCred = JSON.parse(credential) - let clientId = jwkCred.clientId - let tokenEndpoint = jwkCred.tokenEndpoint - let now = Math.floor(new Date().getTime() / 1000) - let plus5Minutes = new Date((now + 5 * 60) * 1000) - let alg = 'RS256' + let jwt = njwt + .create(claims, privateKey, alg) + .setIssuedAt(now) + .setExpiration(plus5Minutes) + .setIssuer(clientId) + .setSubject(clientId) + .compact() - let claims = { - aud: Cypress.env('OIDC_ISSUER') + '/auth/realms/master', + cy.request({ + url: tokenEndpoint, + method: 'POST', + body: { + grant_type: 'client_credentials', + client_id: clientId, + scopes: 'openid', + client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + client_assertion: jwt, + }, + form: true, + }) } +) - let jwt = njwt - .create(claims, privateKey, alg) - .setIssuedAt(now) - .setExpiration(plus5Minutes) - .setIssuer(clientId) - .setSubject(clientId) - .compact() - - cy.request({ - url: tokenEndpoint, - method: 'POST', - body: { - grant_type: 'client_credentials', - client_id: clientId, - scopes: 'openid', - client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', - client_assertion: jwt, - }, - form: true, - }) -}) - -Cypress.Commands.add("generateKeyPair", () => { - const keypair = forge.pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 }); +Cypress.Commands.add('generateKeyPair', () => { + const keypair = forge.pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 }) // Convert the key pair to PEM format - const privateKeyPem = forge.pki.privateKeyToPem(keypair.privateKey); - const publicKeyPem = forge.pki.publicKeyToPem(keypair.publicKey); + const privateKeyPem = forge.pki.privateKeyToPem(keypair.privateKey) + const publicKeyPem = forge.pki.publicKeyToPem(keypair.publicKey) cy.writeFile('cypress/fixtures/state/jwtReGenPrivateKey_new.pem', privateKeyPem) cy.writeFile('cypress/fixtures/state/jwtReGenPublicKey_new.pub', publicKeyPem) - }) Cypress.Commands.add('forceVisit', (url: string) => { - cy.window().then(win => { - return win.open(url, '_self'); - }); -}); - -Cypress.Commands.add('updateJsonBoby', (json: any, key: string, newValue: string): any => { - json[key] = newValue - return json -}); + cy.window().then((win) => { + return win.open(url, '_self') + }) +}) + +Cypress.Commands.add( + 'updateJsonBoby', + (json: any, key: string, newValue: string): any => { + json[key] = newValue + return json + } +) const formDataRequest = ( options: formDataRequestOptions, diff --git a/e2e/cypress/support/global.d.ts b/e2e/cypress/support/global.d.ts index 66c452b27..1f3ce3aa9 100644 --- a/e2e/cypress/support/global.d.ts +++ b/e2e/cypress/support/global.d.ts @@ -21,11 +21,19 @@ declare namespace Cypress { makeKongRequest(serviceName: string, methodType: string, key?: string): Chainable - makeKongGatewayRequestUsingClientIDSecret(hostURL: string, methodType?: string): Chainable + makeKongGatewayRequestUsingClientIDSecret( + hostURL: string, + methodType?: string + ): Chainable preserveCookiesDefaults(): void - saveState(key: string, value: string, flag?: boolean, isGlobal?: boolean): Chainable + saveState( + key: string, + value: string, + flag?: boolean, + isGlobal?: boolean + ): Chainable getState(key: string): Chainable @@ -36,14 +44,29 @@ declare namespace Cypress { client_secret: string ): Chainable> - publishApi(fileNames: any, namespace: string, flag?: boolean): Chainable> + publishApi( + fileNames: any, + namespace: string, + flag?: boolean + ): Chainable> - getServiceOrRouteID(configType: string, host: string + getServiceOrRouteID( + configType: string, + host: string ): Chainable> - updateKongPlugin(pluginName: string, name: string, endPoint?: string, verb?: string): Chainable> + updateKongPlugin( + pluginName: string, + name: string, + endPoint?: string, + verb?: string + ): Chainable> - makeKongGatewayRequest(endpoint: string, requestName: string, methodType: string): Chainable> + makeKongGatewayRequest( + endpoint: string, + requestName: string, + methodType: string + ): Chainable> // generateKeystore() : Chainable @@ -55,25 +78,49 @@ declare namespace Cypress { setAuthorizationToken(token: string): void + callAPI(endPoint: string, methodType: string): Chainable> + makeAPIRequest(endPoint: string, methodType: string): Chainable> getUserSession(): Chainable> - compareJSONObjects(actualResponse: any, expectedResponse: any, indexFlag?: boolean): Chainable> + compareJSONObjects( + actualResponse: any, + expectedResponse: any, + indexFlag?: boolean + ): Chainable> - getUserSessionTokenValue(namespace: string, isNamespaceSelected?: boolean): Chainable> + getUserSessionTokenValue( + namespace: string, + isNamespaceSelected?: boolean + ): Chainable> getUserSessionResponse(): Chainable> - getTokenUsingJWKCredentials(credential: any, privateKey: any): Chainable> + getTokenUsingJWKCredentials( + credential: any, + privateKey: any + ): Chainable> verifyToastMessage(msg: string): Chainable> - updatePluginFile(filename: string, serviceName: string, pluginFileName: string): Chainable> + updatePluginFile( + filename: string, + serviceName: string, + pluginFileName: string + ): Chainable> - updateElementsInPluginFile(filename: string, elementName: string, elementValue: string): Chainable> + updateElementsInPluginFile( + filename: string, + elementName: string, + elementValue: string + ): Chainable> - updatePropertiesOfPluginFile(filename: string, propertyName: any, propertyValue: any): Chainable> + updatePropertiesOfPluginFile( + filename: string, + propertyName: any, + propertyValue: any + ): Chainable> keycloakLogin(username: string, password: string): Chainable @@ -84,27 +131,44 @@ declare namespace Cypress { generateKeyPair(): void // isProductDisplay(productName: string, expResult : boolean) :Chainable> - updateJsonValue(filePath: string, jsonPath: string, newValue: string, index?: any): Chainable - - updateKongPluginForJSONRequest(jsonBody: string, endPoint: string, verb?: string): Chainable> + updateJsonValue( + filePath: string, + jsonPath: string, + newValue: string, + index?: any + ): Chainable + + updateKongPluginForJSONRequest( + jsonBody: string, + endPoint: string, + verb?: string + ): Chainable> forceVisit(url: string): Chainable executeCliCommand(command: string): Chainable - replaceWordInJsonObject(targetWord: string, replacement: string, fileName: string): Chainable> + replaceWordInJsonObject( + targetWord: string, + replacement: string, + fileName: string + ): Chainable> gwaPublish(type: string, fileName: string): Chainable> - replaceWord(originalString: string, wordToReplace: string, replacementWord: string): Chainable + replaceWord( + originalString: string, + wordToReplace: string, + replacementWord: string + ): Chainable + + updateJsonBoby(json: any, key: string, newValue: string): Chainable - updateJsonBoby(json: any, key: string, newValue: string):Chainable + deleteFileInE2EFolder(fileName: string): Chainable - deleteFileInE2EFolder(fileName: string):Chainable + addToAstraScanIdList(item: any): Chainable - addToAstraScanIdList(item: any):Chainable - - checkAstraScanResultForVulnerability():Chainable + checkAstraScanResultForVulnerability(): Chainable makeAPIRequestForScanResult(scanID: string): Chainable> } diff --git a/e2e/cypress/tests/02-client-credential-flow/10-jwt-genkp-access-approve-api-rqst.cy.ts b/e2e/cypress/tests/02-client-credential-flow/10-jwt-genkp-access-approve-api-rqst.cy.ts index 6bc97f8ee..9a10b7965 100644 --- a/e2e/cypress/tests/02-client-credential-flow/10-jwt-genkp-access-approve-api-rqst.cy.ts +++ b/e2e/cypress/tests/02-client-credential-flow/10-jwt-genkp-access-approve-api-rqst.cy.ts @@ -60,7 +60,7 @@ describe('Make an API request using JWT signed with private key', () => { let alg = 'RS256' let claims = { - aud: Cypress.env('OIDC_ISSUER') + '/auth/realms/master', + aud: Cypress.env('OIDC_ISSUER'), } let jwt = njwt @@ -78,7 +78,8 @@ describe('Make an API request using JWT signed with private key', () => { grant_type: 'client_credentials', client_id: clientId, scopes: 'openid', - client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + client_assertion_type: + 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', client_assertion: jwt, }, form: true, diff --git a/e2e/cypress/tests/02-client-credential-flow/14-jwt-publlicKey-access-approve-api-rqst.cy.ts b/e2e/cypress/tests/02-client-credential-flow/14-jwt-publlicKey-access-approve-api-rqst.cy.ts index f9ee17ad9..8fdd22d96 100644 --- a/e2e/cypress/tests/02-client-credential-flow/14-jwt-publlicKey-access-approve-api-rqst.cy.ts +++ b/e2e/cypress/tests/02-client-credential-flow/14-jwt-publlicKey-access-approve-api-rqst.cy.ts @@ -51,53 +51,56 @@ describe('Access manager approves developer access request for JWT - Generated K describe('Make an API request using JWT signed with private key', () => { it('Get access token using JWT key pair; make sure API calls successfully', () => { cy.readFile('cypress/fixtures/state/store.json').then((store_res) => { - cy.readFile('cypress/fixtures/state/jwtReGenPrivateKey_new.pem').then((privateKey) => { - let jwkCred = JSON.parse(store_res.jwksurlcredentials) - let clientId = jwkCred.clientId - let tokenEndpoint = jwkCred.tokenEndpoint + cy.readFile('cypress/fixtures/state/jwtReGenPrivateKey_new.pem').then( + (privateKey) => { + let jwkCred = JSON.parse(store_res.jwksurlcredentials) + let clientId = jwkCred.clientId + let tokenEndpoint = jwkCred.tokenEndpoint - let now = Math.floor(new Date().getTime() / 1000) - let plus5Minutes = new Date((now + 5 * 60) * 1000) - let alg = 'RS256' + let now = Math.floor(new Date().getTime() / 1000) + let plus5Minutes = new Date((now + 5 * 60) * 1000) + let alg = 'RS256' - let claims = { - aud: Cypress.env('OIDC_ISSUER') + '/auth/realms/master', - } + let claims = { + aud: Cypress.env('OIDC_ISSUER'), + } - let jwt = njwt - .create(claims, privateKey, alg) - .setIssuedAt(now) - .setExpiration(plus5Minutes) - .setIssuer(clientId) - .setSubject(clientId) - .compact() + let jwt = njwt + .create(claims, privateKey, alg) + .setIssuedAt(now) + .setExpiration(plus5Minutes) + .setIssuer(clientId) + .setSubject(clientId) + .compact() - cy.request({ - url: tokenEndpoint, - method: 'POST', - body: { - grant_type: 'client_credentials', - client_id: clientId, - scopes: 'openid', - client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', - client_assertion: jwt, - }, - form: true, - }).then((res) => { - let token = res.body.access_token cy.request({ - url: Cypress.env('KONG_URL'), - headers: { - Host: 'cc-service-for-platform.api.gov.bc.ca', - }, - auth: { - bearer: token, + url: tokenEndpoint, + method: 'POST', + body: { + grant_type: 'client_credentials', + client_id: clientId, + scopes: 'openid', + client_assertion_type: + 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + client_assertion: jwt, }, + form: true, }).then((res) => { - expect(res.status).to.eq(200) + let token = res.body.access_token + cy.request({ + url: Cypress.env('KONG_URL'), + headers: { + Host: 'cc-service-for-platform.api.gov.bc.ca', + }, + auth: { + bearer: token, + }, + }).then((res) => { + expect(res.status).to.eq(200) + }) }) - }) - }) + } + ) }) }) }) diff --git a/e2e/cypress/tests/19-api-v3/api-suite.ts b/e2e/cypress/tests/19-api-v3/api-suite.ts new file mode 100644 index 000000000..d76f6ecc8 --- /dev/null +++ b/e2e/cypress/tests/19-api-v3/api-suite.ts @@ -0,0 +1,795 @@ +/** + * Prerequisites: + * - Organization and Organization Unit (ministry-of-health) exists + */ + +const { v4: uuidv4 } = require('uuid') + +function buildGatewayDatasetAndProduct() { + const datasetId = uuidv4().replace(/-/g, '').toUpperCase().substring(0, 4) + + cy.fixture('api-v3').as('api-v3') + return cy.loginByAuthAPI('', '').then(() => { + return cy.get('@loginByAuthApiResponse').then((token_res: any) => { + cy.setHeaders({ 'Content-Type': 'application/json' }) + cy.setAuthorizationToken(token_res.token) + + // just reference the dataset id for easier tracing + const orgId = datasetId + + const org = { + name: `ministry-of-kittens-${orgId}`, + title: 'Some good title about kittens', + description: 'Some good description about kittens', + tags: [], + orgUnits: [ + { + name: `division-of-toys-${orgId}`, + title: 'Division of fun toys to play', + description: 'Some good description about how we manage our toys', + tags: [], + extForeignKey: `division-of-toys-${orgId}`, + extSource: 'internal', + extRecordHash: '', + }, + ], + extSource: 'internal', + extRecordHash: '', + } + + cy.setRequestBody(org) + return cy + .callAPI('ds/api/v3/organizations/ca.bc.gov', 'PUT') + .then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const orgAccess = { + name: org.name, + parent: `/ca.bc.gov`, + members: [ + { + member: { + email: 'janis@testmail.com', + }, + roles: ['organization-admin'], + }, + ], + } + cy.setRequestBody(orgAccess) + + cy.callAPI(`ds/api/v3/organizations/ca.bc.gov/access`, 'PUT').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(204) + } + ) + + const orgUnitAccess = { + name: org.orgUnits[0].name, + parent: `/ca.bc.gov/${org.name}`, + members: [ + { + member: { + email: 'janis@testmail.com', + }, + roles: ['organization-admin'], + }, + ], + } + cy.setRequestBody(orgUnitAccess) + + cy.callAPI(`ds/api/v3/organizations/ca.bc.gov/access`, 'PUT').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(204) + } + ) + + const payload = { + name: `org-dataset-${datasetId}`, + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + download_audience: 'Public', + record_publish_date: '2017-09-05', + notes: 'Some notes', + title: 'A title about my dataset', + tags: ['tag1', 'tag2'], + organization: org.name, + organizationUnit: org.orgUnits[0].name, + } + cy.setRequestBody(payload) + return cy + .callAPI(`ds/api/v3/organizations/${org.name}/datasets`, 'PUT') + .then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { status: 200, result: 'created', childResults: [] } + const datasetId = body.id + delete body.id + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + + return cy + .callAPI( + `ds/api/v3/organizations/${org.name}/datasets/${payload.name}`, + 'GET' + ) + .then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: payload.name, + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + download_audience: 'Public', + record_publish_date: '2017-09-05', + notes: 'Some notes', + title: 'A title about my dataset', + isInCatalog: false, + isDraft: true, + tags: ['tag1', 'tag2'], + organization: { + name: org.name, + title: 'Some good title about kittens', + tags: [], + description: 'Some good description about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + tags: [], + description: 'Some good description about how we manage our toys', + }, + } + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + + cy.setRequestBody({}) + return cy + .callAPI('ds/api/v3/gateways', 'POST') + .then(({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + const myGateway = body + + cy.callAPI( + `ds/api/v3/organizations/${org.name}/${org.orgUnits[0].name}/gateways/${myGateway.gatewayId}?enable=true`, + 'PUT' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(body.result).to.be.equal('namespace-assigned') + }) + + const product = { + name: `my-product-on-${myGateway.gatewayId}`, + dataset: payload.name, + environments: [ + { + name: 'dev', + active: true, + approval: false, + flow: 'public', + }, + ], + } + cy.setRequestBody(product) + + return cy + .callAPI( + `ds/api/v3/gateways/${myGateway.gatewayId}/products`, + 'PUT' + ) + .then(({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + + const match = { + status: 200, + result: 'created', + childResults: [], + } + delete body.id + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + + return { + org, + gateway: myGateway, + dataset: payload, + datasetId, + product, + } + }) + }) + }) + }) + }) + }) + }) +} + +describe('API Directory', () => { + let workingData: any + + before(() => { + buildGatewayDatasetAndProduct().then((data) => { + workingData = data + }) + }) + + it('PUT /organizations/{org}/datasets', () => { + const { org, gateway, dataset, datasetId, product } = workingData + + cy.callAPI(`ds/api/v3/directory/${datasetId}`, 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: dataset.name, + title: 'A title about my dataset', + notes: 'Some notes', + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + tags: ['tag1', 'tag2'], + record_publish_date: '2017-09-05', + isInCatalog: false, + organization: { + name: org.name, + title: 'Some good title about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + }, + products: [ + { + name: `my-product-on-${gateway.gatewayId}`, + environments: [{ name: 'dev', active: true, flow: 'public', services: [] }], + }, + ], + } + delete body.products[0].id + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('GET /directory', () => { + cy.callAPI('ds/api/v3/directory', 'GET').then(({ apiRes: { status, body } }: any) => { + cy.log(`Directory ${JSON.stringify(body, null, 4)}`) + expect(status).to.be.equal(200) + expect(body.length).to.be.greaterThan(0) + }) + }) + + it('GET /directory/{id}', () => { + cy.callAPI('ds/api/v3/directory', 'GET').then(({ apiRes: { status, body } }: any) => { + cy.log(`Directory ${JSON.stringify(body, null, 4)}`) + expect(status).to.be.equal(200) + }) + }) +}) + +describe('API Directory (Gateway Management)', () => { + let workingData: any + + before(() => { + buildGatewayDatasetAndProduct().then((data) => { + workingData = data + }) + }) + + it('GET /gateways/{gatewayId}/datasets/{name}', () => { + const { org, gateway, dataset, datasetId, product } = workingData + cy.callAPI( + `ds/api/v3/gateways/${gateway.gatewayId}/datasets/${dataset.name}`, + 'GET' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: dataset.name, + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + download_audience: 'Public', + record_publish_date: '2017-09-05', + notes: 'Some notes', + title: 'A title about my dataset', + isInCatalog: false, + isDraft: true, + tags: ['tag1', 'tag2'], + organization: { + name: org.name, + title: 'Some good title about kittens', + tags: [], + description: 'Some good description about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + tags: [], + description: 'Some good description about how we manage our toys', + }, + } + + delete body.id + + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) + }) + + it('GET /gateways/{gatewayId}/directory', () => { + const { org, gateway, dataset, datasetId, product } = workingData + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/directory`, 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = [ + { + name: dataset.name, + title: 'A title about my dataset', + notes: 'Some notes', + license_title: 'Open Government Licence - British Columbia', + view_audience: 'Public', + security_class: 'PUBLIC', + record_publish_date: '2017-09-05', + tags: ['tag1', 'tag2'], + organization: { + name: org.name, + title: 'Some good title about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + }, + products: [ + { + name: product.name, + environments: [{ name: 'dev', active: true, flow: 'public' }], + }, + ], + }, + ] + + delete body[0].products[0].id + delete body[0].id + + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('GET /gateways/{gatewayId}/directory/{id}', () => { + const { org, gateway, dataset, datasetId, product } = workingData + cy.callAPI( + `ds/api/v3/gateways/${gateway.gatewayId}/directory/${datasetId}`, + 'GET' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: dataset.name, + title: 'A title about my dataset', + notes: 'Some notes', + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + tags: ['tag1', 'tag2'], + record_publish_date: '2017-09-05', + isInCatalog: false, + organization: { + name: org.name, + title: 'Some good title about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + }, + products: [ + { + name: product.name, + environments: [{ name: 'dev', active: true, flow: 'public', services: [] }], + }, + ], + } + + delete body.products[0].id + delete body.id + + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) + }) +}) + +describe('Organization', () => { + before(() => { + cy.fixture('api-v3').as('api-v3') + cy.loginByAuthAPI('', '').then(() => { + cy.get('@loginByAuthApiResponse').then((token_res: any) => { + cy.setHeaders({ 'Content-Type': 'application/json' }) + cy.setAuthorizationToken(token_res.token) + }) + }) + }) + + it('GET /organizations', () => { + cy.callAPI('ds/api/v3/organizations', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect( + body.filter((o: any) => o.name == 'ministry-of-health').length + ).to.be.equal(1) + } + ) + }) + it('GET /organizations/{org}', () => { + cy.callAPI('ds/api/v3/organizations/ministry-of-health', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect( + body.orgUnits.filter((o: any) => o.name == 'public-health').length + ).to.be.equal(1) + } + ) + }) + + it('GET /organizations/{org}/roles', () => { + const match = { + name: 'ministry-of-health', + parent: '/ca.bc.gov', + roles: [ + { + name: 'organization-admin', + permissions: [ + { + resource: 'org/ministry-of-health', + scopes: ['Dataset.Manage', 'GroupAccess.Manage', 'Namespace.Assign'], + }, + ], + }, + ], + } + + cy.callAPI('ds/api/v3/organizations/ministry-of-health/roles', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('GET /organizations/{org}/access', () => { + const match = { + name: 'ministry-of-health', + parent: '/ca.bc.gov', + members: [], + } + + cy.callAPI('ds/api/v3/organizations/ministry-of-health/access', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('PUT /organizations/{org}/access', () => { + const payload = { + name: 'planning-and-innovation-division', + parent: '/ca.bc.gov/ministry-of-health', + members: [ + { + member: { + email: 'mark@gmail.com', + }, + roles: ['organization-admin'], + }, + ], + } + cy.setRequestBody(payload) + + cy.callAPI( + 'ds/api/v3/organizations/planning-and-innovation-division/access', + 'PUT' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(204) + + cy.callAPI( + 'ds/api/v3/organizations/planning-and-innovation-division/access', + 'GET' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: 'planning-and-innovation-division', + parent: '/ca.bc.gov/ministry-of-health', + members: [ + { + member: { + username: 'mark@idir', + email: 'mark@gmail.com', + }, + roles: ['organization-admin'], + }, + ], + } + // ignore the ID as it will always be different + body.members.forEach((m: any) => { + delete m.member.id + }) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) + }) + }) + + it('GET /organizations/{org}/gateways', () => { + const match = { + name: 'platform', + orgUnit: 'planning-and-innovation-division', + enabled: false, + updatedAt: 0, + } + + cy.callAPI('ds/api/v3/organizations/ministry-of-health/gateways', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect( + JSON.stringify(body.filter((a: any) => a.name == 'platform').pop()) + ).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('GET /organizations/{org}/activity', () => { + cy.callAPI('ds/api/v3/organizations/ministry-of-health/activity', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + // expect(JSON.stringify(body.filter(a => a.params.ns == ))).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('PUT /organizations/{org}/{orgUnit}/gateways/{gatewayId}', () => { + cy.setRequestBody({}) + cy.callAPI('ds/api/v3/gateways', 'POST').then(({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + const myGateway = body + + cy.setRequestBody({}) + cy.callAPI( + `ds/api/v3/organizations/ministry-of-health/planning-and-innovation-division/gateways/${myGateway.gatewayId}?enable=true`, + 'PUT' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(body.result).to.be.equal('namespace-assigned') + }) + }) + }) + + it('GET /roles', () => { + const match: any = { + 'organization-admin': { + label: 'Organization Administrator', + permissions: [ + { + resourceType: 'organization', + scopes: ['GroupAccess.Manage', 'Namespace.Assign', 'Dataset.Manage'], + }, + { resourceType: 'namespace', scopes: ['Namespace.View'] }, + ], + }, + } + + cy.callAPI('ds/api/v3/roles', 'GET').then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) + }) +}) + +describe('Gateways', () => { + let LOCAL: { myGateway?: any } = {} + + before(() => { + cy.fixture('api-v3').as('api-v3') + cy.loginByAuthAPI('', '').then(() => { + cy.get('@loginByAuthApiResponse').then((token_res: any) => { + cy.setHeaders({ 'Content-Type': 'application/json' }) + cy.setAuthorizationToken(token_res.token) + }) + }) + }) + + it('POST /gateways', () => { + const payload = { + displayName: 'My ABC Gateway', + } + cy.setRequestBody(payload) + cy.callAPI('ds/api/v3/gateways', 'POST').then(({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.displayName).to.be.equal(payload.displayName) + LOCAL.myGateway = body + }) + }) + + it('GET /gateways/{gatewayId}', () => { + cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.displayName).to.be.equal(LOCAL.myGateway.displayName) + } + ) + }) + + it('GET /gateways/{gatewayId}/activity', () => { + cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/activity`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.length).to.be.equal(1) + expect(body[0].message).to.be.equal('{actor} created {ns} namespace') + expect(body[0].params.ns).to.be.equal(LOCAL.myGateway.gatewayId) + } + ) + }) + + // it('DELETE /gateways/{gatewayId}', () => { + // cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}`, 'DELETE').then( + // ({ apiRes: { body, status } }: any) => { + // expect(status).to.be.equal(200) + // cy.log(JSON.stringify(body, null, 2)) + // } + // ) + // }) +}) + +describe('Products', () => { + let LOCAL: { myGateway?: any } = {} + + before(() => { + cy.fixture('api-v3').as('api-v3') + cy.loginByAuthAPI('', '').then(() => { + cy.get('@loginByAuthApiResponse').then((token_res: any) => { + cy.setHeaders({ 'Content-Type': 'application/json' }) + cy.setAuthorizationToken(token_res.token) + cy.setRequestBody({}) + cy.callAPI('ds/api/v3/gateways', 'POST').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + LOCAL.myGateway = body + } + ) + }) + }) + }) + + it('PUT /gateways/{gatewayId}/products', () => { + cy.setRequestBody({ + name: `my-product-on-${LOCAL.myGateway.gatewayId}`, + environments: [ + { + name: 'dev', + active: false, + approval: false, + flow: 'public', + }, + ], + }) + cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/products`, 'PUT').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body)) + } + ) + }) + + it('GET /gateways/{gatewayId}/products', () => { + cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/products`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.length).to.be.equal(1) + expect(body[0].name).to.be.equal(`my-product-on-${LOCAL.myGateway.gatewayId}`) + expect(body[0].environments.length).to.be.equal(1) + } + ) + }) +}) + +describe('Authorization Profiles', () => { + let LOCAL: { myGateway?: any } = {} + + before(() => { + cy.fixture('api-v3').as('api-v3') + cy.loginByAuthAPI('', '').then(() => { + cy.get('@loginByAuthApiResponse').then((token_res: any) => { + cy.setHeaders({ 'Content-Type': 'application/json' }) + cy.setAuthorizationToken(token_res.token) + cy.setRequestBody({}) + cy.callAPI('ds/api/v3/gateways', 'POST').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + LOCAL.myGateway = body + } + ) + }) + }) + }) + + it('PUT /gateways/{gatewayId}/issuers', () => { + cy.setRequestBody({ + name: `my-auth-profile-for-${LOCAL.myGateway.gatewayId}`, + description: 'Auth connection to my IdP', + flow: 'client-credentials', + clientAuthenticator: 'client-secret', + mode: 'auto', + inheritFrom: 'Sample Shared IdP', + }) + cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/issuers`, 'PUT').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body)) + } + ) + }) + + it('GET /gateways/{gatewayId}/issuers', () => { + cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/issuers`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.length).to.be.equal(1) + + const issuer = body[0] + + expect(issuer.name).to.be.equal( + `my-auth-profile-for-${LOCAL.myGateway.gatewayId}` + ) + expect(issuer.environmentDetails[0].environment).to.be.equal('test') + expect(issuer.environmentDetails[0].issuerUrl).to.be.equal( + Cypress.env('OIDC_ISSUER') + ) + expect(issuer.environmentDetails[0].clientId).to.be.equal( + `ap-my-auth-profile-for-${LOCAL.myGateway.gatewayId}-test` + ) + } + ) + }) +}) + +describe('Identifiers', () => { + it('GET /identifiers/application', () => { + cy.callAPI('ds/api/v3/identifiers/application', 'GET').then( + ({ apiRes: { status, body } }: any) => { + cy.log(`ID ${body}`) + expect(status).to.be.equal(200) + } + ) + }) + + it('GET /identifiers/product', () => { + cy.callAPI('ds/api/v3/identifiers/product', 'GET').then( + ({ apiRes: { status, body } }: any) => { + cy.log(`ID ${body}`) + expect(status).to.be.equal(200) + } + ) + }) + + it('GET /identifiers/environment', () => { + cy.callAPI('ds/api/v3/identifiers/environment', 'GET').then( + ({ apiRes: { status, body } }: any) => { + cy.log(`ID ${body}`) + expect(status).to.be.equal(200) + } + ) + }) + + it('GET /identifiers/gateway', () => { + cy.callAPI('ds/api/v3/identifiers/gateway', 'GET').then( + ({ apiRes: { status, body } }: any) => { + cy.log(`ID ${body}`) + expect(status).to.be.equal(200) + } + ) + }) +}) diff --git a/e2e/package-lock.json b/e2e/package-lock.json index da494f9c1..5c265fe6b 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -26,6 +26,7 @@ "npm-run-all": "^4.1.5", "request": "^2.88.2", "typescript": "^4.3.5", + "uuid": "^8.3.2", "yaml": "^2.1.3", "yamljs": "^0.3.0" }, @@ -119,11 +120,11 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dependencies": { - "@babel/highlight": "^7.24.2", + "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" }, "engines": { @@ -131,28 +132,28 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", - "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", - "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.24.5", - "@babel/helpers": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -176,11 +177,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", - "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", "dependencies": { - "@babel/types": "^7.24.5", + "@babel/types": "^7.24.7", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -190,12 +191,12 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -213,57 +214,61 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dependencies": { + "@babel/types": "^7.24.7" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dependencies": { - "@babel/types": "^7.24.0" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", - "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-simple-access": "^7.24.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/helper-validator-identifier": "^7.24.5" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -273,78 +278,78 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", - "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", - "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dependencies": { - "@babel/types": "^7.24.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dependencies": { - "@babel/types": "^7.24.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", - "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -410,9 +415,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -421,31 +426,31 @@ } }, "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", - "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", - "dependencies": { - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/types": "^7.24.5", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -462,12 +467,12 @@ } }, "node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -875,9 +880,9 @@ } }, "node_modules/@types/node": { - "version": "16.18.97", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.97.tgz", - "integrity": "sha512-4muilE1Lbfn57unR+/nT9AFjWk0MtWi5muwCEJqnOvfRQDbSfLCUdN7vCIg8TYuaANfhLOV85ve+FNpiUsbSRg==", + "version": "16.18.98", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.98.tgz", + "integrity": "sha512-fpiC20NvLpTLAzo3oVBKIqBGR6Fx/8oAK/SSf7G+fydnXMY1x4x9RZ6sBXhqKlCU21g2QapUsbLlhv3+a7wS+Q==", "dev": true }, "node_modules/@types/request": { @@ -1884,6 +1889,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1968,9 +1974,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001621", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001621.tgz", - "integrity": "sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==", + "version": "1.0.30001628", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001628.tgz", + "integrity": "sha512-S3BnR4Kh26TBxbi5t5kpbcUlLJb9lhtDXISDPwOfI+JoC+ik0QksvkZtUVyikw3hjnkgkMPSJ8oIM9yMm9vflA==", "funding": [ { "type": "opencollective", @@ -2386,9 +2392,9 @@ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "node_modules/cypress": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.10.0.tgz", - "integrity": "sha512-tOhwRlurVOQbMduX+KonoMeQILs2cwR3yHGGENoFvvSoLUBHmJ8b9/n21gFSDqjlOJ+SRVcwuh+fG/JDsHsT6Q==", + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.11.0.tgz", + "integrity": "sha512-NXXogbAxVlVje4XHX+Cx5eMFZv4Dho/2rIcdBHg9CNPFUGZdM4cRdgIgM7USmNYsC12XY0bZENEQ+KBk72fl+A==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -2844,9 +2850,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.779", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.779.tgz", - "integrity": "sha512-oaTiIcszNfySXVJzKcjxd2YjPxziAd+GmXyb2HbidCeFo6Z88ygOT7EimlrEQhM2U08VhSrbKhLOXP0kKUCZ6g==" + "version": "1.4.790", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.790.tgz", + "integrity": "sha512-eVGeQxpaBYbomDBa/Mehrs28MdvCXfJmEFzaMFsv8jH/MJDLIylJN81eTJ5kvx7B7p18OiPK0BkC06lydEy63A==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -3981,6 +3987,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -7184,6 +7191,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dependencies": { "glob": "^7.1.3" }, @@ -7684,9 +7692,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==" + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==" }, "node_modules/sprintf-js": { "version": "1.1.3", @@ -8062,9 +8070,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, "node_modules/tsutils": { @@ -8646,9 +8654,9 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", - "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.3.tgz", + "integrity": "sha512-sntgmxj8o7DE7g/Qi60cqpLBA3HG3STcDA0kO+WfB05jEKhZMbY7umNm2rBpQvsmZ16/lPXCJGW2672dgOUkrg==", "bin": { "yaml": "bin.mjs" }, diff --git a/e2e/package.json b/e2e/package.json index a88a6812c..c8c5664b1 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -64,6 +64,7 @@ "npm-run-all": "^4.1.5", "request": "^2.88.2", "typescript": "^4.3.5", + "uuid": "^8.3.2", "yaml": "^2.1.3", "yamljs": "^0.3.0" } diff --git a/local/keycloak/master-realm.json b/local/keycloak/master-realm.json index df8ae3277..80e884ff3 100644 --- a/local/keycloak/master-realm.json +++ b/local/keycloak/master-realm.json @@ -2071,7 +2071,7 @@ "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "config": { - "scopes": "[\"GroupAccess.Manage\",\"Namespace.Assign\"]", + "scopes": "[\"GroupAccess.Manage\",\"Namespace.Assign\",\"Dataset.Manage\"]", "applyPolicies": "[\"janis\"]" } }, @@ -2317,9 +2317,7 @@ } } ], - "defaultClientScopes": [ - "profile" - ], + "defaultClientScopes": ["profile"], "optionalClientScopes": [] }, { diff --git a/src/auth/auth-tsoa.ts b/src/auth/auth-tsoa.ts index 7f9decf70..ad837461c 100644 --- a/src/auth/auth-tsoa.ts +++ b/src/auth/auth-tsoa.ts @@ -80,7 +80,12 @@ export function expressAuthentication( request, { status: (s: number) => { - logger.error('invalid_token (%d) for %j', s, request.oauth_user); + logger.error( + 'invalid_token (%d) [%j] for %j', + s, + permissions, + request.oauth_user + ); reject( new UnauthorizedError('invalid_token', { message: `Missing authorization scope. (${s})`, diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index 40f87b964..6dea337a9 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -15,7 +15,7 @@ const metadata = { transformations: { tags: { name: 'toStringDefaultArray' }, orgUnits: { - name: 'connectExclusiveList', + name: 'connectExclusiveListCreate', list: 'OrganizationUnit', syncFirst: true, }, diff --git a/src/batch/feed-worker.ts b/src/batch/feed-worker.ts index 2d0ce490a..f82c4eeaa 100644 --- a/src/batch/feed-worker.ts +++ b/src/batch/feed-worker.ts @@ -677,6 +677,17 @@ export const removeKeys = (obj: object, keys: string[]) => { return obj; }; +export const replaceKey = (obj: object, oldKey: string, newKey: string) => { + Object.entries(obj).forEach( + ([key, val]) => + (oldKey == key && + delete (obj as any)[key] && + ((obj as any)[newKey] = val)) || + (val && typeof val === 'object' && replaceKey(val, oldKey, newKey)) + ); + return obj; +}; + export const removeAllButKeys = (obj: object, keys: string[]) => { Object.entries(obj).forEach( ([key, val]) => diff --git a/src/controllers/ioc/keystoneInjector.ts b/src/controllers/ioc/keystoneInjector.ts index a29f9dd4c..1da31cf8d 100644 --- a/src/controllers/ioc/keystoneInjector.ts +++ b/src/controllers/ioc/keystoneInjector.ts @@ -47,7 +47,7 @@ export class KeystoneService { id: null, name: resolveName(request.user), username: resolveUsername(request.user), - namespace: request.params.ns, + namespace: request.params.ns || request.params.gatewayId, roles: JSON.stringify(scopesToRoles(identityProvider, _scopes)), scopes: _scopes, userId: null, diff --git a/src/controllers/v3/GatewayController.ts b/src/controllers/v3/GatewayController.ts index d44802a2d..863ea21bb 100644 --- a/src/controllers/v3/GatewayController.ts +++ b/src/controllers/v3/GatewayController.ts @@ -36,7 +36,7 @@ import { getActivity } from '../../services/keystone/activity'; import { transformActivity } from '../../services/workflow'; import { ActivityDetail } from './types-extra'; -const logger = Logger('controllers.Namespace'); +const logger = Logger('controllers.Gateway'); /** * @param binary Buffer @@ -152,8 +152,9 @@ export class NamespaceController extends Controller { @Security('jwt', []) public async create( @Request() request: any, - @Body() vars: NamespaceInput + @Body() vars: Gateway ): Promise { + logger.debug('Input %j', vars); const result = await this.keystone.executeGraphQL({ context: this.keystone.createContext(request), query: createNS, @@ -165,6 +166,7 @@ export class NamespaceController extends Controller { result.errors.forEach((err: any, ind: number) => { errors[`d${ind}`] = { message: err.message }; }); + logger.error('%j', result); throw new ValidateError(errors, 'Unable to create namespace'); } return { @@ -202,6 +204,7 @@ export class NamespaceController extends Controller { result.errors.forEach((err: any, ind: number) => { errors[`d${ind}`] = { message: err.message }; }); + logger.error('%j', result); throw new ValidateError(errors, 'Unable to delete gateway'); } return result.data.forceDeleteNamespace; @@ -249,8 +252,8 @@ const list = gql` `; const item = gql` - query Namespace($gatewayId: String!) { - namespace(gatewayId: $ns) { + query Namespace($ns: String!) { + namespace(ns: $ns) { name displayName scopes { @@ -269,7 +272,7 @@ const item = gql` `; const deleteNS = gql` - mutation ForceDeleteNamespace($gatewayId: String!, $force: Boolean!) { + mutation ForceDeleteNamespace($ns: String!, $force: Boolean!) { forceDeleteNamespace(namespace: $ns, force: $force) } `; diff --git a/src/controllers/v3/IdentifierController.ts b/src/controllers/v3/IdentifierController.ts index b37fc2657..7bed441d4 100644 --- a/src/controllers/v3/IdentifierController.ts +++ b/src/controllers/v3/IdentifierController.ts @@ -4,6 +4,7 @@ import { newProductID, newEnvironmentID, newApplicationID, + newGatewayID, } from '../../services/identifiers'; @Route('identifiers') @@ -11,10 +12,12 @@ import { export class IdentifiersController extends Controller { @Get('{type}') public async getNewID( - @Path() type: 'environment' | 'product' | 'application' + @Path() type: 'environment' | 'product' | 'application' | 'gateway' ): Promise { if (type == 'environment') { return newEnvironmentID(); + } else if (type == 'gateway') { + return newGatewayID(); } else if (type == 'product') { return newProductID(); } else if (type == 'application') { diff --git a/src/controllers/v3/OrganizationController.ts b/src/controllers/v3/OrganizationController.ts index ab7585daf..6f60c5ade 100644 --- a/src/controllers/v3/OrganizationController.ts +++ b/src/controllers/v3/OrganizationController.ts @@ -11,9 +11,9 @@ import { Body, Get, Tags, + Post, } from 'tsoa'; import { KeystoneService } from '../ioc/keystoneInjector'; -import { assertEqual } from '../ioc/assert'; import { inject, injectable } from 'tsyringe'; import { syncRecords, @@ -22,6 +22,7 @@ import { removeEmpty, removeKeys, transformAllRefID, + syncRecordsThrowErrors, } from '../../batch/feed-worker'; import { GroupAccessService, @@ -39,10 +40,12 @@ import { } from '../../services/org-groups/types'; import { getOrganizations, getOrganizationUnit } from '../../services/keystone'; import { getActivity } from '../../services/keystone/activity'; -import { Activity } from './types'; +import { Activity, Organization } from './types'; import { isParent } from '../../services/org-groups/group-converter-utils'; import { ActivitySummary } from '../../services/keystone/types'; import { ActivityDetail } from './types-extra'; +import { BatchResult } from '../../batch/types'; +import { assertEqual } from '../ioc/assert'; @injectable() @Route('/organizations') @@ -65,6 +68,37 @@ export class OrganizationController extends Controller { })); } + /** + * Create Organization + * > `Required Scope:` GroupAccess.Manage + * + * @summary Create Organizations + * @param ns + * @param body + * @param request + */ + @Put('{org}') + @OperationId('put-organization') + @Security('jwt', ['GroupAccess.Manage']) + public async post( + @Path() org: string, + @Body() body: Organization, + @Request() request: any + ): Promise { + assertEqual( + org == 'ca.bc.gov', + true, + 'org', + 'Only root level is allowed to do this operation' + ); + return await syncRecordsThrowErrors( + this.keystone.createContext(request, true), + 'Organization', + body['name'], + body + ); + } + @Get('{org}') @OperationId('organization-units') public async listOrganizationUnits(@Path() org: string): Promise { diff --git a/src/controllers/v3/ProductController.ts b/src/controllers/v3/ProductController.ts index d78484ee7..81a10de66 100644 --- a/src/controllers/v3/ProductController.ts +++ b/src/controllers/v3/ProductController.ts @@ -25,6 +25,7 @@ import { deleteRecord, getRecord, transformArrayKeyToString, + replaceKey, } from '../../batch/feed-worker'; import { Product } from './types'; import { BatchResult } from '../../batch/types'; @@ -69,7 +70,7 @@ export class ProductController extends Controller { this.keystone.createContext(request), 'Product', body['appId'], - body + replaceKey(body, 'gatewayId', 'namespace') ); } @@ -204,6 +205,7 @@ export class ProductController extends Controller { result.errors.forEach((err: any, ind: number) => { errors[`d${ind}`] = { message: err.message }; }); + logger.error('%j', result); throw new ValidateError(errors, 'Unable to delete product environment'); } return result.data.forceDeleteEnvironment; diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 0210e15a3..56fc2cc74 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -152,16 +152,6 @@ components: type: string type: object additionalProperties: false - Maybe_Scalars-at-String_: - type: string - nullable: true - NamespaceInput: - properties: - displayName: - $ref: '#/components/schemas/Maybe_Scalars-at-String_' - name: - $ref: '#/components/schemas/Maybe_Scalars-at-String_' - type: object ActivityDetail: properties: id: @@ -352,6 +342,54 @@ components: mode: auto environmentDetails: [] owner: janis@gov.bc.ca + OrganizationUnit: + properties: + extForeignKey: + type: string + name: + type: string + sector: + type: string + title: + type: string + description: + type: string + extSource: + type: string + extRecordHash: + type: string + tags: + items: + type: string + type: array + type: object + additionalProperties: false + Organization: + properties: + extForeignKey: + type: string + name: + type: string + sector: + type: string + title: + type: string + description: + type: string + extSource: + type: string + extRecordHash: + type: string + tags: + items: + type: string + type: array + orgUnits: + items: + $ref: '#/components/schemas/OrganizationUnit' + type: array + type: object + additionalProperties: false GroupPermission: properties: resource: @@ -822,7 +860,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/NamespaceInput' + $ref: '#/components/schemas/Gateway' '/gateways/{gatewayId}': get: operationId: namespace-profile @@ -1056,6 +1094,7 @@ paths: - environment - product - application + - gateway '/gateways/{gatewayId}/issuers': put: operationId: put-issuer @@ -1160,6 +1199,36 @@ paths: security: [] parameters: [] '/organizations/{org}': + put: + operationId: put-organization + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Create Organization\n> `Required Scope:` GroupAccess.Manage" + summary: 'Create Organizations' + tags: + - Organizations + security: + - + jwt: + - GroupAccess.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Organization' get: operationId: organization-units responses: @@ -1557,6 +1626,3 @@ tags: - name: 'Authorization Profiles' description: 'Configure the integration to external Identity Providers' - - - name: Documentation - description: 'View public documentation and publish documentation for your APIs' diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index c8383682e..1af730ae5 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -115,16 +115,6 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Maybe_Scalars-at-String_": { - "dataType": "refAlias", - "type": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"validators":{}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "NamespaceInput": { - "dataType": "refAlias", - "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"displayName":{"ref":"Maybe_Scalars-at-String_"},"name":{"ref":"Maybe_Scalars-at-String_"}},"validators":{}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "ActivityDetail": { "dataType": "refObject", "properties": { @@ -235,6 +225,37 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "OrganizationUnit": { + "dataType": "refObject", + "properties": { + "extForeignKey": {"dataType":"string"}, + "name": {"dataType":"string"}, + "sector": {"dataType":"string"}, + "title": {"dataType":"string"}, + "description": {"dataType":"string"}, + "extSource": {"dataType":"string"}, + "extRecordHash": {"dataType":"string"}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Organization": { + "dataType": "refObject", + "properties": { + "extForeignKey": {"dataType":"string"}, + "name": {"dataType":"string"}, + "sector": {"dataType":"string"}, + "title": {"dataType":"string"}, + "description": {"dataType":"string"}, + "extSource": {"dataType":"string"}, + "extRecordHash": {"dataType":"string"}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + "orgUnits": {"dataType":"array","array":{"dataType":"refObject","ref":"OrganizationUnit"}}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "GroupPermission": { "dataType": "refObject", "properties": { @@ -691,7 +712,7 @@ export function RegisterRoutes(app: express.Router) { async function NamespaceController_create(request: any, response: any, next: any) { const args = { request: {"in":"request","name":"request","required":true,"dataType":"object"}, - vars: {"in":"body","name":"vars","required":true,"ref":"NamespaceInput"}, + vars: {"in":"body","name":"vars","required":true,"ref":"Gateway"}, }; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa @@ -903,7 +924,7 @@ export function RegisterRoutes(app: express.Router) { async function IdentifiersController_getNewID(request: any, response: any, next: any) { const args = { - type: {"in":"path","name":"type","required":true,"dataType":"union","subSchemas":[{"dataType":"enum","enums":["environment"]},{"dataType":"enum","enums":["product"]},{"dataType":"enum","enums":["application"]}]}, + type: {"in":"path","name":"type","required":true,"dataType":"union","subSchemas":[{"dataType":"enum","enums":["environment"]},{"dataType":"enum","enums":["product"]},{"dataType":"enum","enums":["application"]},{"dataType":"enum","enums":["gateway"]}]}, }; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa @@ -1046,6 +1067,37 @@ export function RegisterRoutes(app: express.Router) { } }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.put('/ds/api/v3/organizations/:org', + authenticateMiddleware([{"jwt":["GroupAccess.Manage"]}]), + + async function OrganizationController_post(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"Organization"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.post.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.get('/ds/api/v3/organizations/:org', async function OrganizationController_listOrganizationUnits(request: any, response: any, next: any) { diff --git a/src/services/identifiers.ts b/src/services/identifiers.ts index 24af1f39b..06e8f7d17 100644 --- a/src/services/identifiers.ts +++ b/src/services/identifiers.ts @@ -23,3 +23,5 @@ export function newEnvironmentID(): string { export function newNamespaceID(): string { return 'gw-' + uuidv4().replace(/-/g, '').toLowerCase().substring(0, 5); } + +export const newGatewayID = newNamespaceID; diff --git a/src/services/keycloak/group-service.ts b/src/services/keycloak/group-service.ts index 3e9127369..45a72b7ca 100644 --- a/src/services/keycloak/group-service.ts +++ b/src/services/keycloak/group-service.ts @@ -163,6 +163,24 @@ export class KeycloakGroupService { return this.kcAdminClient.groups.findOne({ id }); } + public async hasGroup(parentGroupName: string, groupName: string) { + const listOfGroups = this.allGroups + ? this.allGroups + : await this.kcAdminClient.groups.find(); + const groups = listOfGroups.filter( + (group: GroupRepresentation) => group.name == parentGroupName + ); + if ( + groups[0].subGroups.filter( + (group: GroupRepresentation) => group.name == groupName + ).length == 0 + ) { + return false; + } else { + return true; + } + } + public async getGroup(parentGroupName: string, groupName: string) { const listOfGroups = this.allGroups ? this.allGroups diff --git a/src/services/org-groups/namespace.ts b/src/services/org-groups/namespace.ts index 958eb93c2..ef321f51f 100644 --- a/src/services/org-groups/namespace.ts +++ b/src/services/org-groups/namespace.ts @@ -139,8 +139,8 @@ export class NamespaceService { } async checkNamespaceAvailable(ns: string): Promise { - const group = await this.groupService.getGroup('ns', ns); - assert.strictEqual(group === null, true, 'Namespace already exists'); + const groupExists: boolean = await this.groupService.hasGroup('ns', ns); + assert.strictEqual(groupExists, false, 'Namespace already exists'); } async listAssignedNamespacesByOrg(org: string): Promise { diff --git a/src/test/services/batch/batch-utils.test.js b/src/test/services/batch/batch-utils.test.js index 907c6c7d1..7eca7d11c 100644 --- a/src/test/services/batch/batch-utils.test.js +++ b/src/test/services/batch/batch-utils.test.js @@ -5,6 +5,7 @@ import { removeEmpty, removeKeys, dot, + replaceKey, } from '../../../batch/feed-worker'; import YAML from 'js-yaml'; @@ -137,4 +138,34 @@ describe('Batch Utilities', function () { expect(dot(value, '.nowhere')).toBe(null); expect(dot(value, 'a.b.c')).toBe(null); }); + + it('should replace key', async function () { + const input = { + gatewayId: 'gw-1234', + name: 'sample name', + attribute: null, + block: { + type: 'bland', + }, + nested: { + id: '000011', + another: null, + }, + }; + const output = { + name: 'sample name', + attribute: null, + block: { + type: 'bland', + }, + nested: { + id: '000011', + another: null, + }, + namespace: 'gw-1234', + }; + + const result = replaceKey(input, 'gatewayId', 'namespace'); + expect(JSON.stringify(result)).toBe(JSON.stringify(output)); + }); }); diff --git a/src/tsoa-v3.json b/src/tsoa-v3.json index 10e323533..06d33b2f6 100644 --- a/src/tsoa-v3.json +++ b/src/tsoa-v3.json @@ -56,10 +56,6 @@ { "name": "Authorization Profiles", "description": "Configure the integration to external Identity Providers" - }, - { - "name": "Documentation", - "description": "View public documentation and publish documentation for your APIs" } ] }, From 8c77220dc81e2521cb7d389251696bb5e53a46a2 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 5 Jun 2024 13:40:00 -0700 Subject: [PATCH 044/191] cleanup bit of cypress --- e2e/cypress/fixtures/api-v3.json | 6 ------ e2e/cypress/tests/19-api-v3/api-suite.ts | 5 ----- 2 files changed, 11 deletions(-) delete mode 100644 e2e/cypress/fixtures/api-v3.json diff --git a/e2e/cypress/fixtures/api-v3.json b/e2e/cypress/fixtures/api-v3.json deleted file mode 100644 index 9ecb8f180..000000000 --- a/e2e/cypress/fixtures/api-v3.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "gateway": { - "displayName": "My Gateway" - }, - "model": [] -} diff --git a/e2e/cypress/tests/19-api-v3/api-suite.ts b/e2e/cypress/tests/19-api-v3/api-suite.ts index d76f6ecc8..7db6e9f09 100644 --- a/e2e/cypress/tests/19-api-v3/api-suite.ts +++ b/e2e/cypress/tests/19-api-v3/api-suite.ts @@ -8,7 +8,6 @@ const { v4: uuidv4 } = require('uuid') function buildGatewayDatasetAndProduct() { const datasetId = uuidv4().replace(/-/g, '').toUpperCase().substring(0, 4) - cy.fixture('api-v3').as('api-v3') return cy.loginByAuthAPI('', '').then(() => { return cy.get('@loginByAuthApiResponse').then((token_res: any) => { cy.setHeaders({ 'Content-Type': 'application/json' }) @@ -400,7 +399,6 @@ describe('API Directory (Gateway Management)', () => { describe('Organization', () => { before(() => { - cy.fixture('api-v3').as('api-v3') cy.loginByAuthAPI('', '').then(() => { cy.get('@loginByAuthApiResponse').then((token_res: any) => { cy.setHeaders({ 'Content-Type': 'application/json' }) @@ -588,7 +586,6 @@ describe('Gateways', () => { let LOCAL: { myGateway?: any } = {} before(() => { - cy.fixture('api-v3').as('api-v3') cy.loginByAuthAPI('', '').then(() => { cy.get('@loginByAuthApiResponse').then((token_res: any) => { cy.setHeaders({ 'Content-Type': 'application/json' }) @@ -646,7 +643,6 @@ describe('Products', () => { let LOCAL: { myGateway?: any } = {} before(() => { - cy.fixture('api-v3').as('api-v3') cy.loginByAuthAPI('', '').then(() => { cy.get('@loginByAuthApiResponse').then((token_res: any) => { cy.setHeaders({ 'Content-Type': 'application/json' }) @@ -699,7 +695,6 @@ describe('Authorization Profiles', () => { let LOCAL: { myGateway?: any } = {} before(() => { - cy.fixture('api-v3').as('api-v3') cy.loginByAuthAPI('', '').then(() => { cy.get('@loginByAuthApiResponse').then((token_res: any) => { cy.setHeaders({ 'Content-Type': 'application/json' }) From ab31df504919328f132b5bb5045621b79ffb7ffd Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Wed, 5 Jun 2024 13:54:58 -0700 Subject: [PATCH 045/191] new page for get-started --- .../pages/manager/namespaces/get-started.tsx | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/nextapp/pages/manager/namespaces/get-started.tsx diff --git a/src/nextapp/pages/manager/namespaces/get-started.tsx b/src/nextapp/pages/manager/namespaces/get-started.tsx new file mode 100644 index 000000000..8c1ae2507 --- /dev/null +++ b/src/nextapp/pages/manager/namespaces/get-started.tsx @@ -0,0 +1,49 @@ +import GatewayGetStarted from '@/components/gateway-get-started'; +import PageHeader from '@/components/page-header'; +import { useApi } from '@/shared/services/api'; +import { + Container, + Heading +} from '@chakra-ui/react'; +import { gql } from 'graphql-request'; +import Head from 'next/head'; +import React from 'react'; + +const NamespacesPage: React.FC = () => { + const { data, isSuccess, isError } = useApi( + 'allNamespaces', + { query }, + { suspense: false } + ); + + return ( + <> + + + API Program Services | My Gateways + + + + + <> + {isError && ( + Gateways Failed to Load + )} + {isSuccess && data.allNamespaces.length == 0 && ( + + )} + + + + ); +}; + +export default NamespacesPage; + +const query = gql` + query GetNamespaces { + allNamespaces { + name + } + } +`; \ No newline at end of file From d02a5ba70061ceedd235d193a7f79f86b9d73f85 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Wed, 5 Jun 2024 14:04:02 -0700 Subject: [PATCH 046/191] added Get Started to links to show in nav for dev --- src/nextapp/pages/manager/namespaces/get-started.tsx | 4 +++- src/nextapp/shared/data/links.ts | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/nextapp/pages/manager/namespaces/get-started.tsx b/src/nextapp/pages/manager/namespaces/get-started.tsx index 8c1ae2507..1e8f936b2 100644 --- a/src/nextapp/pages/manager/namespaces/get-started.tsx +++ b/src/nextapp/pages/manager/namespaces/get-started.tsx @@ -29,7 +29,9 @@ const NamespacesPage: React.FC = () => { {isError && ( Gateways Failed to Load )} - {isSuccess && data.allNamespaces.length == 0 && ( + {/* TODO: add data.allNamespaces.length == 0 to the router logic in order to show this page */} + {/* {isSuccess && data.allNamespaces.length == 0 && ( */} + {isSuccess && ( )} diff --git a/src/nextapp/shared/data/links.ts b/src/nextapp/shared/data/links.ts index 57140dff8..3180ae586 100644 --- a/src/nextapp/shared/data/links.ts +++ b/src/nextapp/shared/data/links.ts @@ -53,6 +53,12 @@ const links: NavLink[] = [ access: ['portal-user'], sites: ['devportal'], }, + { + name: 'Gateways Get Started', + url: '/manager/namespaces/get-started', + access: ['portal-user'], + sites: ['devportal'], + }, { name: 'Namespaces', altUrls: [ From 8fb827c956bad942b0386085664cf5fad1c9f720 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 6 Jun 2024 10:44:17 -0700 Subject: [PATCH 047/191] divide up api v2 tests --- e2e/cypress/support/auth-commands.ts | 52 +- e2e/cypress/support/e2e.ts | 1 + e2e/cypress/support/global.d.ts | 2 + e2e/cypress/support/prep-commands.ts | 204 +++++ .../tests/19-api-v3/01-api-directory.ts | 193 +++++ .../tests/19-api-v3/02-organization.ts | 182 ++++ e2e/cypress/tests/19-api-v3/03-gateways.ts | 75 ++ e2e/cypress/tests/19-api-v3/04-products.ts | 43 + e2e/cypress/tests/19-api-v3/05-issuers.ts | 49 ++ e2e/cypress/tests/19-api-v3/06-identifiers.ts | 37 + e2e/cypress/tests/19-api-v3/api-suite.ts | 790 ------------------ 11 files changed, 813 insertions(+), 815 deletions(-) create mode 100644 e2e/cypress/support/prep-commands.ts create mode 100644 e2e/cypress/tests/19-api-v3/01-api-directory.ts create mode 100644 e2e/cypress/tests/19-api-v3/02-organization.ts create mode 100644 e2e/cypress/tests/19-api-v3/03-gateways.ts create mode 100644 e2e/cypress/tests/19-api-v3/04-products.ts create mode 100644 e2e/cypress/tests/19-api-v3/05-issuers.ts create mode 100644 e2e/cypress/tests/19-api-v3/06-identifiers.ts delete mode 100644 e2e/cypress/tests/19-api-v3/api-suite.ts diff --git a/e2e/cypress/support/auth-commands.ts b/e2e/cypress/support/auth-commands.ts index 5256a563f..c2c6d4c48 100644 --- a/e2e/cypress/support/auth-commands.ts +++ b/e2e/cypress/support/auth-commands.ts @@ -119,7 +119,7 @@ Cypress.Commands.add('resetCredential', (accessRole: string) => { Cypress.Commands.add( 'getUserSessionTokenValue', - (namespace: string, isNamespaceSelected?: true) => { + (namespace: string, isNamespaceSelected?: boolean) => { const login = new LoginPage() const home = new HomePage() const na = new NamespaceAccessPage() @@ -170,37 +170,39 @@ Cypress.Commands.add('getSession', () => { cy.log('> Get Session') }) -Cypress.Commands.add('loginByAuthAPI', (username: string, password: string) => { +Cypress.Commands.add('loginByAuthAPI', (username: string, password: string): any => { const log = Cypress.log({ displayName: 'AUTH0 LOGIN', message: [`🔐 Authenticating | ${username}`], autoEnd: false, }) log.snapshot('before') - cy.request({ - method: 'POST', - url: Cypress.env('OIDC_ISSUER') + '/protocol/openid-connect/token', - body: { - grant_type: 'password', - username: Cypress.env('DEV_USERNAME'), - password: Cypress.env('DEV_PASSWORD'), - Scope: 'openid', - client_id: Cypress.env('CLIENT_ID'), - client_secret: Cypress.env('CLIENT_SECRET'), - }, - form: true, - }).then(({ body }: any) => { - const user: any = jwt.decode(body.id_token) - const userItem = { - token: body.access_token, - user: { - ...user, + return cy + .request({ + method: 'POST', + url: Cypress.env('OIDC_ISSUER') + '/protocol/openid-connect/token', + body: { + grant_type: 'password', + username: Cypress.env('DEV_USERNAME'), + password: Cypress.env('DEV_PASSWORD'), + Scope: 'openid', + client_id: Cypress.env('CLIENT_ID'), + client_secret: Cypress.env('CLIENT_SECRET'), }, - } - cy.wrap(userItem).as('loginByAuthApiResponse') - }) - log.snapshot('after') - log.end() + form: true, + }) + .then(({ body }: any) => { + const user: any = jwt.decode(body.id_token) + const userItem = { + token: body.access_token, + user: { + ...user, + }, + } + log.snapshot('after') + log.end() + return userItem + }) }) Cypress.Commands.add('logout', () => { diff --git a/e2e/cypress/support/e2e.ts b/e2e/cypress/support/e2e.ts index 997e3ea59..57598e6a0 100644 --- a/e2e/cypress/support/e2e.ts +++ b/e2e/cypress/support/e2e.ts @@ -1,6 +1,7 @@ import './commands' import 'cypress-xpath' import './auth-commands' +import './prep-commands' import './util-commands' import '@cypress/code-coverage/support' const _ = require('lodash') diff --git a/e2e/cypress/support/global.d.ts b/e2e/cypress/support/global.d.ts index 1f3ce3aa9..a3591c2e7 100644 --- a/e2e/cypress/support/global.d.ts +++ b/e2e/cypress/support/global.d.ts @@ -171,5 +171,7 @@ declare namespace Cypress { checkAstraScanResultForVulnerability(): Chainable makeAPIRequestForScanResult(scanID: string): Chainable> + + buildOrgGatewayDatasetAndProduct(): Chainable> } } diff --git a/e2e/cypress/support/prep-commands.ts b/e2e/cypress/support/prep-commands.ts new file mode 100644 index 000000000..c7f8562bd --- /dev/null +++ b/e2e/cypress/support/prep-commands.ts @@ -0,0 +1,204 @@ +const { v4: uuidv4 } = require('uuid') + +Cypress.Commands.add('buildOrgGatewayDatasetAndProduct', (): Cypress.Chainable => { + const datasetId = uuidv4().replace(/-/g, '').toUpperCase().substring(0, 4) + + return cy.loginByAuthAPI('', '').then((token_res: any) => { + cy.setHeaders({ 'Content-Type': 'application/json' }) + cy.setAuthorizationToken(token_res.token) + + // just reference the dataset id for easier tracing + const orgId = datasetId + + const org = { + name: `ministry-of-kittens-${orgId}`, + title: 'Some good title about kittens', + description: 'Some good description about kittens', + tags: [], + orgUnits: [ + { + name: `division-of-toys-${orgId}`, + title: 'Division of fun toys to play', + description: 'Some good description about how we manage our toys', + tags: [], + extForeignKey: `division-of-toys-${orgId}`, + extSource: 'internal', + extRecordHash: '', + }, + ], + extSource: 'internal', + extRecordHash: '', + } + + // New organization and org unit + cy.setRequestBody(org) + return cy + .callAPI('ds/api/v3/organizations/ca.bc.gov', 'PUT') + .then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const orgAccess = { + name: org.name, + parent: `/ca.bc.gov`, + members: [ + { + member: { + email: 'janis@testmail.com', + }, + roles: ['organization-admin'], + }, + ], + } + cy.setRequestBody(orgAccess) + + // Set permissions for the new Org + cy.callAPI(`ds/api/v3/organizations/ca.bc.gov/access`, 'PUT').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(204) + } + ) + + const orgUnitAccess = { + name: org.orgUnits[0].name, + parent: `/ca.bc.gov/${org.name}`, + members: [ + { + member: { + email: 'janis@testmail.com', + }, + roles: ['organization-admin'], + }, + ], + } + cy.setRequestBody(orgUnitAccess) + + // Set permissions for the new Org Unit + cy.callAPI(`ds/api/v3/organizations/ca.bc.gov/access`, 'PUT').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(204) + } + ) + + const payload = { + name: `org-dataset-${datasetId}`, + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + download_audience: 'Public', + record_publish_date: '2017-09-05', + notes: 'Some notes', + title: 'A title about my dataset', + tags: ['tag1', 'tag2'], + organization: org.name, + organizationUnit: org.orgUnits[0].name, + } + cy.setRequestBody(payload) + + // New dataset + return cy + .callAPI(`ds/api/v3/organizations/${org.name}/datasets`, 'PUT') + .then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { status: 200, result: 'created', childResults: [] } + const datasetId = body.id + delete body.id + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + + return cy + .callAPI( + `ds/api/v3/organizations/${org.name}/datasets/${payload.name}`, + 'GET' + ) + .then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: payload.name, + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + download_audience: 'Public', + record_publish_date: '2017-09-05', + notes: 'Some notes', + title: 'A title about my dataset', + isInCatalog: false, + isDraft: true, + tags: ['tag1', 'tag2'], + organization: { + name: org.name, + title: 'Some good title about kittens', + tags: [], + description: 'Some good description about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + tags: [], + description: 'Some good description about how we manage our toys', + }, + } + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + + // New Gateway + cy.setRequestBody({}) + return cy + .callAPI('ds/api/v3/gateways', 'POST') + .then(({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + const myGateway = body + + // Assign gateway to Org + cy.callAPI( + `ds/api/v3/organizations/${org.name}/${org.orgUnits[0].name}/gateways/${myGateway.gatewayId}?enable=true`, + 'PUT' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(body.result).to.be.equal('namespace-assigned') + }) + + const product = { + name: `my-product-on-${myGateway.gatewayId}`, + dataset: payload.name, + environments: [ + { + name: 'dev', + active: true, + approval: false, + flow: 'public', + }, + ], + } + cy.setRequestBody(product) + + // New Product and Active Environment + return cy + .callAPI( + `ds/api/v3/gateways/${myGateway.gatewayId}/products`, + 'PUT' + ) + .then(({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + + const match = { + status: 200, + result: 'created', + childResults: [], + } + delete body.id + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + + return { + org, + gateway: myGateway, + dataset: payload, + datasetId, + product, + } + }) + }) + }) + }) + }) + }) +}) diff --git a/e2e/cypress/tests/19-api-v3/01-api-directory.ts b/e2e/cypress/tests/19-api-v3/01-api-directory.ts new file mode 100644 index 000000000..75d86259c --- /dev/null +++ b/e2e/cypress/tests/19-api-v3/01-api-directory.ts @@ -0,0 +1,193 @@ +describe('API Directory', () => { + let workingData: any + + before(() => { + cy.buildOrgGatewayDatasetAndProduct().then((data) => { + workingData = data + }) + }) + + it('PUT /organizations/{org}/datasets', () => { + const { org, gateway, dataset, datasetId, product } = workingData + cy.callAPI(`ds/api/v3/directory/${datasetId}`, 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: dataset.name, + title: 'A title about my dataset', + notes: 'Some notes', + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + tags: ['tag1', 'tag2'], + record_publish_date: '2017-09-05', + isInCatalog: false, + organization: { + name: org.name, + title: 'Some good title about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + }, + products: [ + { + name: `my-product-on-${gateway.gatewayId}`, + environments: [{ name: 'dev', active: true, flow: 'public', services: [] }], + }, + ], + } + delete body.products[0].id + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('GET /directory', () => { + cy.callAPI('ds/api/v3/directory', 'GET').then(({ apiRes: { status, body } }: any) => { + cy.log(`Directory ${JSON.stringify(body, null, 4)}`) + expect(status).to.be.equal(200) + expect(body.length).to.be.greaterThan(0) + }) + }) + + it('GET /directory/{id}', () => { + cy.callAPI('ds/api/v3/directory', 'GET').then(({ apiRes: { status, body } }: any) => { + cy.log(`Directory ${JSON.stringify(body, null, 4)}`) + expect(status).to.be.equal(200) + }) + }) +}) + +describe('API Directory (Gateway Management)', () => { + let workingData: any + + before(() => { + cy.buildOrgGatewayDatasetAndProduct().then((data) => { + workingData = data + }) + }) + + it('GET /gateways/{gatewayId}/datasets/{name}', () => { + const { org, gateway, dataset, datasetId, product } = workingData + cy.callAPI( + `ds/api/v3/gateways/${gateway.gatewayId}/datasets/${dataset.name}`, + 'GET' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: dataset.name, + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + download_audience: 'Public', + record_publish_date: '2017-09-05', + notes: 'Some notes', + title: 'A title about my dataset', + isInCatalog: false, + isDraft: true, + tags: ['tag1', 'tag2'], + organization: { + name: org.name, + title: 'Some good title about kittens', + tags: [], + description: 'Some good description about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + tags: [], + description: 'Some good description about how we manage our toys', + }, + } + + delete body.id + + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) + }) + + it('GET /gateways/{gatewayId}/directory', () => { + const { org, gateway, dataset, datasetId, product } = workingData + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/directory`, 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = [ + { + name: dataset.name, + title: 'A title about my dataset', + notes: 'Some notes', + license_title: 'Open Government Licence - British Columbia', + view_audience: 'Public', + security_class: 'PUBLIC', + record_publish_date: '2017-09-05', + tags: ['tag1', 'tag2'], + organization: { + name: org.name, + title: 'Some good title about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + }, + products: [ + { + name: product.name, + environments: [{ name: 'dev', active: true, flow: 'public' }], + }, + ], + }, + ] + + delete body[0].products[0].id + delete body[0].id + + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('GET /gateways/{gatewayId}/directory/{id}', () => { + const { org, gateway, dataset, datasetId, product } = workingData + cy.callAPI( + `ds/api/v3/gateways/${gateway.gatewayId}/directory/${datasetId}`, + 'GET' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: dataset.name, + title: 'A title about my dataset', + notes: 'Some notes', + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + tags: ['tag1', 'tag2'], + record_publish_date: '2017-09-05', + isInCatalog: false, + organization: { + name: org.name, + title: 'Some good title about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + }, + products: [ + { + name: product.name, + environments: [{ name: 'dev', active: true, flow: 'public', services: [] }], + }, + ], + } + + delete body.products[0].id + delete body.id + + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) + }) +}) diff --git a/e2e/cypress/tests/19-api-v3/02-organization.ts b/e2e/cypress/tests/19-api-v3/02-organization.ts new file mode 100644 index 000000000..4f20d5f99 --- /dev/null +++ b/e2e/cypress/tests/19-api-v3/02-organization.ts @@ -0,0 +1,182 @@ +describe('Organization', () => { + before(() => { + cy.loginByAuthAPI('', '').then((token_res: any) => { + cy.setHeaders({ 'Content-Type': 'application/json' }) + cy.setAuthorizationToken(token_res.token) + }) + }) + + it('GET /organizations', () => { + cy.callAPI('ds/api/v3/organizations', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect( + body.filter((o: any) => o.name == 'ministry-of-health').length + ).to.be.equal(1) + } + ) + }) + it('GET /organizations/{org}', () => { + cy.callAPI('ds/api/v3/organizations/ministry-of-health', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect( + body.orgUnits.filter((o: any) => o.name == 'public-health').length + ).to.be.equal(1) + } + ) + }) + + it('GET /organizations/{org}/roles', () => { + const match = { + name: 'ministry-of-health', + parent: '/ca.bc.gov', + roles: [ + { + name: 'organization-admin', + permissions: [ + // { + // resource: 'org/ministry-of-health', + // scopes: ['Dataset.Manage', 'GroupAccess.Manage', 'Namespace.Assign'], + // }, + ], + }, + ], + } + + cy.callAPI('ds/api/v3/organizations/ministry-of-health/roles', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('GET /organizations/{org}/access', () => { + const match = { + name: 'ministry-of-health', + parent: '/ca.bc.gov', + members: [], + } + + cy.callAPI('ds/api/v3/organizations/ministry-of-health/access', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('PUT /organizations/{org}/access', () => { + const payload = { + name: 'planning-and-innovation-division', + parent: '/ca.bc.gov/ministry-of-health', + members: [ + { + member: { + email: 'mark@gmail.com', + }, + roles: ['organization-admin'], + }, + ], + } + cy.setRequestBody(payload) + + cy.callAPI( + 'ds/api/v3/organizations/planning-and-innovation-division/access', + 'PUT' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(204) + + cy.callAPI( + 'ds/api/v3/organizations/planning-and-innovation-division/access', + 'GET' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: 'planning-and-innovation-division', + parent: '/ca.bc.gov/ministry-of-health', + members: [ + { + member: { + username: 'mark@idir', + email: 'mark@gmail.com', + }, + roles: ['organization-admin'], + }, + ], + } + // ignore the ID as it will always be different + body.members.forEach((m: any) => { + delete m.member.id + }) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) + }) + }) + + it('GET /organizations/{org}/gateways', () => { + const match = { + name: 'platform', + orgUnit: 'planning-and-innovation-division', + enabled: false, + updatedAt: 0, + } + + cy.callAPI('ds/api/v3/organizations/ministry-of-health/gateways', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect( + JSON.stringify(body.filter((a: any) => a.name == 'platform').pop()) + ).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('GET /organizations/{org}/activity', () => { + cy.callAPI('ds/api/v3/organizations/ministry-of-health/activity', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + // expect(JSON.stringify(body.filter(a => a.params.ns == ))).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('PUT /organizations/{org}/{orgUnit}/gateways/{gatewayId}', () => { + cy.setRequestBody({}) + cy.callAPI('ds/api/v3/gateways', 'POST').then(({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + const myGateway = body + + cy.setRequestBody({}) + cy.callAPI( + `ds/api/v3/organizations/ministry-of-health/planning-and-innovation-division/gateways/${myGateway.gatewayId}?enable=true`, + 'PUT' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(body.result).to.be.equal('namespace-assigned') + }) + }) + }) + + it('GET /roles', () => { + const match: any = { + 'organization-admin': { + label: 'Organization Administrator', + permissions: [ + { + resourceType: 'organization', + scopes: ['GroupAccess.Manage', 'Namespace.Assign', 'Dataset.Manage'], + }, + { resourceType: 'namespace', scopes: ['Namespace.View'] }, + ], + }, + } + + cy.callAPI('ds/api/v3/roles', 'GET').then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) + }) +}) diff --git a/e2e/cypress/tests/19-api-v3/03-gateways.ts b/e2e/cypress/tests/19-api-v3/03-gateways.ts new file mode 100644 index 000000000..8fb858056 --- /dev/null +++ b/e2e/cypress/tests/19-api-v3/03-gateways.ts @@ -0,0 +1,75 @@ +describe('Gateways', () => { + let workingData: any + + before(() => { + cy.buildOrgGatewayDatasetAndProduct().then((data) => { + workingData = data + }) + }) + + it('POST /gateways', () => { + const payload = { + displayName: 'My ABC Gateway', + } + cy.setRequestBody(payload) + cy.callAPI('ds/api/v3/gateways', 'POST').then(({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.displayName).to.be.equal(payload.displayName) + + const gateway = body + + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.displayName).to.be.equal(gateway.displayName) + } + ) + + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/activity`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.length).to.be.equal(1) + expect(body[0].message).to.be.equal('{actor} created {ns} namespace') + expect(body[0].params.ns).to.be.equal(gateway.gatewayId) + } + ) + }) + }) + + it('GET /gateways/{gatewayId}', () => { + const { gateway } = workingData + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.displayName).to.be.equal(gateway.displayName) + } + ) + }) + + it('GET /gateways/{gatewayId}/activity', () => { + const { gateway } = workingData + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/activity`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.length).to.be.equal(3) + expect(body[2].message).to.be.equal('{actor} created {ns} namespace') + expect(body[2].params.ns).to.be.equal(gateway.gatewayId) + } + ) + }) + + // it('DELETE /gateways/{gatewayId}', () => { + // const { gateway } = workingData + // cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}`, 'DELETE').then( + // ({ apiRes: { body, status } }: any) => { + // expect(status).to.be.equal(200) + // cy.log(JSON.stringify(body, null, 2)) + // } + // ) + // }) +}) diff --git a/e2e/cypress/tests/19-api-v3/04-products.ts b/e2e/cypress/tests/19-api-v3/04-products.ts new file mode 100644 index 000000000..e4338cfae --- /dev/null +++ b/e2e/cypress/tests/19-api-v3/04-products.ts @@ -0,0 +1,43 @@ +describe('Products', () => { + let workingData: any + + before(() => { + cy.buildOrgGatewayDatasetAndProduct().then((data) => { + workingData = data + }) + }) + + it('PUT /gateways/{gatewayId}/products', () => { + const { gateway } = workingData + cy.setRequestBody({ + name: `my-product-on-${gateway.gatewayId}`, + environments: [ + { + name: 'dev', + active: false, + approval: false, + flow: 'public', + }, + ], + }) + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/products`, 'PUT').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body)) + } + ) + }) + + it('GET /gateways/{gatewayId}/products', () => { + const { gateway } = workingData + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/products`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.length).to.be.equal(1) + expect(body[0].name).to.be.equal(`my-product-on-${gateway.gatewayId}`) + expect(body[0].environments.length).to.be.equal(1) + } + ) + }) +}) diff --git a/e2e/cypress/tests/19-api-v3/05-issuers.ts b/e2e/cypress/tests/19-api-v3/05-issuers.ts new file mode 100644 index 000000000..433ca3dd3 --- /dev/null +++ b/e2e/cypress/tests/19-api-v3/05-issuers.ts @@ -0,0 +1,49 @@ +describe('Authorization Profiles', () => { + let workingData: any + + before(() => { + cy.buildOrgGatewayDatasetAndProduct().then((data) => { + workingData = data + }) + }) + + it('PUT /gateways/{gatewayId}/issuers', () => { + const { gateway } = workingData + cy.setRequestBody({ + name: `my-auth-profile-for-${gateway.gatewayId}`, + description: 'Auth connection to my IdP', + flow: 'client-credentials', + clientAuthenticator: 'client-secret', + mode: 'auto', + inheritFrom: 'Sample Shared IdP', + }) + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/issuers`, 'PUT').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body)) + } + ) + }) + + it('GET /gateways/{gatewayId}/issuers', () => { + const { gateway } = workingData + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/issuers`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.length).to.be.equal(1) + + const issuer = body[0] + + expect(issuer.name).to.be.equal(`my-auth-profile-for-${gateway.gatewayId}`) + expect(issuer.environmentDetails[0].environment).to.be.equal('test') + expect(issuer.environmentDetails[0].issuerUrl).to.be.equal( + Cypress.env('OIDC_ISSUER') + ) + expect(issuer.environmentDetails[0].clientId).to.be.equal( + `ap-my-auth-profile-for-${gateway.gatewayId}-test` + ) + } + ) + }) +}) diff --git a/e2e/cypress/tests/19-api-v3/06-identifiers.ts b/e2e/cypress/tests/19-api-v3/06-identifiers.ts new file mode 100644 index 000000000..421770b21 --- /dev/null +++ b/e2e/cypress/tests/19-api-v3/06-identifiers.ts @@ -0,0 +1,37 @@ +describe('Identifiers', () => { + it('GET /identifiers/application', () => { + cy.callAPI('ds/api/v3/identifiers/application', 'GET').then( + ({ apiRes: { status, body } }: any) => { + cy.log(`ID ${body}`) + expect(status).to.be.equal(200) + } + ) + }) + + it('GET /identifiers/product', () => { + cy.callAPI('ds/api/v3/identifiers/product', 'GET').then( + ({ apiRes: { status, body } }: any) => { + cy.log(`ID ${body}`) + expect(status).to.be.equal(200) + } + ) + }) + + it('GET /identifiers/environment', () => { + cy.callAPI('ds/api/v3/identifiers/environment', 'GET').then( + ({ apiRes: { status, body } }: any) => { + cy.log(`ID ${body}`) + expect(status).to.be.equal(200) + } + ) + }) + + it('GET /identifiers/gateway', () => { + cy.callAPI('ds/api/v3/identifiers/gateway', 'GET').then( + ({ apiRes: { status, body } }: any) => { + cy.log(`ID ${body}`) + expect(status).to.be.equal(200) + } + ) + }) +}) diff --git a/e2e/cypress/tests/19-api-v3/api-suite.ts b/e2e/cypress/tests/19-api-v3/api-suite.ts deleted file mode 100644 index 7db6e9f09..000000000 --- a/e2e/cypress/tests/19-api-v3/api-suite.ts +++ /dev/null @@ -1,790 +0,0 @@ -/** - * Prerequisites: - * - Organization and Organization Unit (ministry-of-health) exists - */ - -const { v4: uuidv4 } = require('uuid') - -function buildGatewayDatasetAndProduct() { - const datasetId = uuidv4().replace(/-/g, '').toUpperCase().substring(0, 4) - - return cy.loginByAuthAPI('', '').then(() => { - return cy.get('@loginByAuthApiResponse').then((token_res: any) => { - cy.setHeaders({ 'Content-Type': 'application/json' }) - cy.setAuthorizationToken(token_res.token) - - // just reference the dataset id for easier tracing - const orgId = datasetId - - const org = { - name: `ministry-of-kittens-${orgId}`, - title: 'Some good title about kittens', - description: 'Some good description about kittens', - tags: [], - orgUnits: [ - { - name: `division-of-toys-${orgId}`, - title: 'Division of fun toys to play', - description: 'Some good description about how we manage our toys', - tags: [], - extForeignKey: `division-of-toys-${orgId}`, - extSource: 'internal', - extRecordHash: '', - }, - ], - extSource: 'internal', - extRecordHash: '', - } - - cy.setRequestBody(org) - return cy - .callAPI('ds/api/v3/organizations/ca.bc.gov', 'PUT') - .then(({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - - const orgAccess = { - name: org.name, - parent: `/ca.bc.gov`, - members: [ - { - member: { - email: 'janis@testmail.com', - }, - roles: ['organization-admin'], - }, - ], - } - cy.setRequestBody(orgAccess) - - cy.callAPI(`ds/api/v3/organizations/ca.bc.gov/access`, 'PUT').then( - ({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(204) - } - ) - - const orgUnitAccess = { - name: org.orgUnits[0].name, - parent: `/ca.bc.gov/${org.name}`, - members: [ - { - member: { - email: 'janis@testmail.com', - }, - roles: ['organization-admin'], - }, - ], - } - cy.setRequestBody(orgUnitAccess) - - cy.callAPI(`ds/api/v3/organizations/ca.bc.gov/access`, 'PUT').then( - ({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(204) - } - ) - - const payload = { - name: `org-dataset-${datasetId}`, - license_title: 'Open Government Licence - British Columbia', - security_class: 'PUBLIC', - view_audience: 'Public', - download_audience: 'Public', - record_publish_date: '2017-09-05', - notes: 'Some notes', - title: 'A title about my dataset', - tags: ['tag1', 'tag2'], - organization: org.name, - organizationUnit: org.orgUnits[0].name, - } - cy.setRequestBody(payload) - return cy - .callAPI(`ds/api/v3/organizations/${org.name}/datasets`, 'PUT') - .then(({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - - const match = { status: 200, result: 'created', childResults: [] } - const datasetId = body.id - delete body.id - expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) - - return cy - .callAPI( - `ds/api/v3/organizations/${org.name}/datasets/${payload.name}`, - 'GET' - ) - .then(({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - - const match = { - name: payload.name, - license_title: 'Open Government Licence - British Columbia', - security_class: 'PUBLIC', - view_audience: 'Public', - download_audience: 'Public', - record_publish_date: '2017-09-05', - notes: 'Some notes', - title: 'A title about my dataset', - isInCatalog: false, - isDraft: true, - tags: ['tag1', 'tag2'], - organization: { - name: org.name, - title: 'Some good title about kittens', - tags: [], - description: 'Some good description about kittens', - }, - organizationUnit: { - name: org.orgUnits[0].name, - title: 'Division of fun toys to play', - tags: [], - description: 'Some good description about how we manage our toys', - }, - } - expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) - - cy.setRequestBody({}) - return cy - .callAPI('ds/api/v3/gateways', 'POST') - .then(({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - const myGateway = body - - cy.callAPI( - `ds/api/v3/organizations/${org.name}/${org.orgUnits[0].name}/gateways/${myGateway.gatewayId}?enable=true`, - 'PUT' - ).then(({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - expect(body.result).to.be.equal('namespace-assigned') - }) - - const product = { - name: `my-product-on-${myGateway.gatewayId}`, - dataset: payload.name, - environments: [ - { - name: 'dev', - active: true, - approval: false, - flow: 'public', - }, - ], - } - cy.setRequestBody(product) - - return cy - .callAPI( - `ds/api/v3/gateways/${myGateway.gatewayId}/products`, - 'PUT' - ) - .then(({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - - const match = { - status: 200, - result: 'created', - childResults: [], - } - delete body.id - expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) - - return { - org, - gateway: myGateway, - dataset: payload, - datasetId, - product, - } - }) - }) - }) - }) - }) - }) - }) -} - -describe('API Directory', () => { - let workingData: any - - before(() => { - buildGatewayDatasetAndProduct().then((data) => { - workingData = data - }) - }) - - it('PUT /organizations/{org}/datasets', () => { - const { org, gateway, dataset, datasetId, product } = workingData - - cy.callAPI(`ds/api/v3/directory/${datasetId}`, 'GET').then( - ({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - - const match = { - name: dataset.name, - title: 'A title about my dataset', - notes: 'Some notes', - license_title: 'Open Government Licence - British Columbia', - security_class: 'PUBLIC', - view_audience: 'Public', - tags: ['tag1', 'tag2'], - record_publish_date: '2017-09-05', - isInCatalog: false, - organization: { - name: org.name, - title: 'Some good title about kittens', - }, - organizationUnit: { - name: org.orgUnits[0].name, - title: 'Division of fun toys to play', - }, - products: [ - { - name: `my-product-on-${gateway.gatewayId}`, - environments: [{ name: 'dev', active: true, flow: 'public', services: [] }], - }, - ], - } - delete body.products[0].id - expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) - } - ) - }) - - it('GET /directory', () => { - cy.callAPI('ds/api/v3/directory', 'GET').then(({ apiRes: { status, body } }: any) => { - cy.log(`Directory ${JSON.stringify(body, null, 4)}`) - expect(status).to.be.equal(200) - expect(body.length).to.be.greaterThan(0) - }) - }) - - it('GET /directory/{id}', () => { - cy.callAPI('ds/api/v3/directory', 'GET').then(({ apiRes: { status, body } }: any) => { - cy.log(`Directory ${JSON.stringify(body, null, 4)}`) - expect(status).to.be.equal(200) - }) - }) -}) - -describe('API Directory (Gateway Management)', () => { - let workingData: any - - before(() => { - buildGatewayDatasetAndProduct().then((data) => { - workingData = data - }) - }) - - it('GET /gateways/{gatewayId}/datasets/{name}', () => { - const { org, gateway, dataset, datasetId, product } = workingData - cy.callAPI( - `ds/api/v3/gateways/${gateway.gatewayId}/datasets/${dataset.name}`, - 'GET' - ).then(({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - - const match = { - name: dataset.name, - license_title: 'Open Government Licence - British Columbia', - security_class: 'PUBLIC', - view_audience: 'Public', - download_audience: 'Public', - record_publish_date: '2017-09-05', - notes: 'Some notes', - title: 'A title about my dataset', - isInCatalog: false, - isDraft: true, - tags: ['tag1', 'tag2'], - organization: { - name: org.name, - title: 'Some good title about kittens', - tags: [], - description: 'Some good description about kittens', - }, - organizationUnit: { - name: org.orgUnits[0].name, - title: 'Division of fun toys to play', - tags: [], - description: 'Some good description about how we manage our toys', - }, - } - - delete body.id - - expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) - }) - }) - - it('GET /gateways/{gatewayId}/directory', () => { - const { org, gateway, dataset, datasetId, product } = workingData - cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/directory`, 'GET').then( - ({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - - const match = [ - { - name: dataset.name, - title: 'A title about my dataset', - notes: 'Some notes', - license_title: 'Open Government Licence - British Columbia', - view_audience: 'Public', - security_class: 'PUBLIC', - record_publish_date: '2017-09-05', - tags: ['tag1', 'tag2'], - organization: { - name: org.name, - title: 'Some good title about kittens', - }, - organizationUnit: { - name: org.orgUnits[0].name, - title: 'Division of fun toys to play', - }, - products: [ - { - name: product.name, - environments: [{ name: 'dev', active: true, flow: 'public' }], - }, - ], - }, - ] - - delete body[0].products[0].id - delete body[0].id - - expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) - } - ) - }) - - it('GET /gateways/{gatewayId}/directory/{id}', () => { - const { org, gateway, dataset, datasetId, product } = workingData - cy.callAPI( - `ds/api/v3/gateways/${gateway.gatewayId}/directory/${datasetId}`, - 'GET' - ).then(({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - - const match = { - name: dataset.name, - title: 'A title about my dataset', - notes: 'Some notes', - license_title: 'Open Government Licence - British Columbia', - security_class: 'PUBLIC', - view_audience: 'Public', - tags: ['tag1', 'tag2'], - record_publish_date: '2017-09-05', - isInCatalog: false, - organization: { - name: org.name, - title: 'Some good title about kittens', - }, - organizationUnit: { - name: org.orgUnits[0].name, - title: 'Division of fun toys to play', - }, - products: [ - { - name: product.name, - environments: [{ name: 'dev', active: true, flow: 'public', services: [] }], - }, - ], - } - - delete body.products[0].id - delete body.id - - expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) - }) - }) -}) - -describe('Organization', () => { - before(() => { - cy.loginByAuthAPI('', '').then(() => { - cy.get('@loginByAuthApiResponse').then((token_res: any) => { - cy.setHeaders({ 'Content-Type': 'application/json' }) - cy.setAuthorizationToken(token_res.token) - }) - }) - }) - - it('GET /organizations', () => { - cy.callAPI('ds/api/v3/organizations', 'GET').then( - ({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - expect( - body.filter((o: any) => o.name == 'ministry-of-health').length - ).to.be.equal(1) - } - ) - }) - it('GET /organizations/{org}', () => { - cy.callAPI('ds/api/v3/organizations/ministry-of-health', 'GET').then( - ({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - expect( - body.orgUnits.filter((o: any) => o.name == 'public-health').length - ).to.be.equal(1) - } - ) - }) - - it('GET /organizations/{org}/roles', () => { - const match = { - name: 'ministry-of-health', - parent: '/ca.bc.gov', - roles: [ - { - name: 'organization-admin', - permissions: [ - { - resource: 'org/ministry-of-health', - scopes: ['Dataset.Manage', 'GroupAccess.Manage', 'Namespace.Assign'], - }, - ], - }, - ], - } - - cy.callAPI('ds/api/v3/organizations/ministry-of-health/roles', 'GET').then( - ({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) - } - ) - }) - - it('GET /organizations/{org}/access', () => { - const match = { - name: 'ministry-of-health', - parent: '/ca.bc.gov', - members: [], - } - - cy.callAPI('ds/api/v3/organizations/ministry-of-health/access', 'GET').then( - ({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) - } - ) - }) - - it('PUT /organizations/{org}/access', () => { - const payload = { - name: 'planning-and-innovation-division', - parent: '/ca.bc.gov/ministry-of-health', - members: [ - { - member: { - email: 'mark@gmail.com', - }, - roles: ['organization-admin'], - }, - ], - } - cy.setRequestBody(payload) - - cy.callAPI( - 'ds/api/v3/organizations/planning-and-innovation-division/access', - 'PUT' - ).then(({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(204) - - cy.callAPI( - 'ds/api/v3/organizations/planning-and-innovation-division/access', - 'GET' - ).then(({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - - const match = { - name: 'planning-and-innovation-division', - parent: '/ca.bc.gov/ministry-of-health', - members: [ - { - member: { - username: 'mark@idir', - email: 'mark@gmail.com', - }, - roles: ['organization-admin'], - }, - ], - } - // ignore the ID as it will always be different - body.members.forEach((m: any) => { - delete m.member.id - }) - expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) - }) - }) - }) - - it('GET /organizations/{org}/gateways', () => { - const match = { - name: 'platform', - orgUnit: 'planning-and-innovation-division', - enabled: false, - updatedAt: 0, - } - - cy.callAPI('ds/api/v3/organizations/ministry-of-health/gateways', 'GET').then( - ({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - expect( - JSON.stringify(body.filter((a: any) => a.name == 'platform').pop()) - ).to.be.equal(JSON.stringify(match)) - } - ) - }) - - it('GET /organizations/{org}/activity', () => { - cy.callAPI('ds/api/v3/organizations/ministry-of-health/activity', 'GET').then( - ({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - // expect(JSON.stringify(body.filter(a => a.params.ns == ))).to.be.equal(JSON.stringify(match)) - } - ) - }) - - it('PUT /organizations/{org}/{orgUnit}/gateways/{gatewayId}', () => { - cy.setRequestBody({}) - cy.callAPI('ds/api/v3/gateways', 'POST').then(({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - const myGateway = body - - cy.setRequestBody({}) - cy.callAPI( - `ds/api/v3/organizations/ministry-of-health/planning-and-innovation-division/gateways/${myGateway.gatewayId}?enable=true`, - 'PUT' - ).then(({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - expect(body.result).to.be.equal('namespace-assigned') - }) - }) - }) - - it('GET /roles', () => { - const match: any = { - 'organization-admin': { - label: 'Organization Administrator', - permissions: [ - { - resourceType: 'organization', - scopes: ['GroupAccess.Manage', 'Namespace.Assign', 'Dataset.Manage'], - }, - { resourceType: 'namespace', scopes: ['Namespace.View'] }, - ], - }, - } - - cy.callAPI('ds/api/v3/roles', 'GET').then(({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) - }) - }) -}) - -describe('Gateways', () => { - let LOCAL: { myGateway?: any } = {} - - before(() => { - cy.loginByAuthAPI('', '').then(() => { - cy.get('@loginByAuthApiResponse').then((token_res: any) => { - cy.setHeaders({ 'Content-Type': 'application/json' }) - cy.setAuthorizationToken(token_res.token) - }) - }) - }) - - it('POST /gateways', () => { - const payload = { - displayName: 'My ABC Gateway', - } - cy.setRequestBody(payload) - cy.callAPI('ds/api/v3/gateways', 'POST').then(({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - cy.log(JSON.stringify(body, null, 2)) - expect(body.displayName).to.be.equal(payload.displayName) - LOCAL.myGateway = body - }) - }) - - it('GET /gateways/{gatewayId}', () => { - cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}`, 'GET').then( - ({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - cy.log(JSON.stringify(body, null, 2)) - expect(body.displayName).to.be.equal(LOCAL.myGateway.displayName) - } - ) - }) - - it('GET /gateways/{gatewayId}/activity', () => { - cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/activity`, 'GET').then( - ({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - cy.log(JSON.stringify(body, null, 2)) - expect(body.length).to.be.equal(1) - expect(body[0].message).to.be.equal('{actor} created {ns} namespace') - expect(body[0].params.ns).to.be.equal(LOCAL.myGateway.gatewayId) - } - ) - }) - - // it('DELETE /gateways/{gatewayId}', () => { - // cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}`, 'DELETE').then( - // ({ apiRes: { body, status } }: any) => { - // expect(status).to.be.equal(200) - // cy.log(JSON.stringify(body, null, 2)) - // } - // ) - // }) -}) - -describe('Products', () => { - let LOCAL: { myGateway?: any } = {} - - before(() => { - cy.loginByAuthAPI('', '').then(() => { - cy.get('@loginByAuthApiResponse').then((token_res: any) => { - cy.setHeaders({ 'Content-Type': 'application/json' }) - cy.setAuthorizationToken(token_res.token) - cy.setRequestBody({}) - cy.callAPI('ds/api/v3/gateways', 'POST').then( - ({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - LOCAL.myGateway = body - } - ) - }) - }) - }) - - it('PUT /gateways/{gatewayId}/products', () => { - cy.setRequestBody({ - name: `my-product-on-${LOCAL.myGateway.gatewayId}`, - environments: [ - { - name: 'dev', - active: false, - approval: false, - flow: 'public', - }, - ], - }) - cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/products`, 'PUT').then( - ({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - cy.log(JSON.stringify(body)) - } - ) - }) - - it('GET /gateways/{gatewayId}/products', () => { - cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/products`, 'GET').then( - ({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - cy.log(JSON.stringify(body, null, 2)) - expect(body.length).to.be.equal(1) - expect(body[0].name).to.be.equal(`my-product-on-${LOCAL.myGateway.gatewayId}`) - expect(body[0].environments.length).to.be.equal(1) - } - ) - }) -}) - -describe('Authorization Profiles', () => { - let LOCAL: { myGateway?: any } = {} - - before(() => { - cy.loginByAuthAPI('', '').then(() => { - cy.get('@loginByAuthApiResponse').then((token_res: any) => { - cy.setHeaders({ 'Content-Type': 'application/json' }) - cy.setAuthorizationToken(token_res.token) - cy.setRequestBody({}) - cy.callAPI('ds/api/v3/gateways', 'POST').then( - ({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - LOCAL.myGateway = body - } - ) - }) - }) - }) - - it('PUT /gateways/{gatewayId}/issuers', () => { - cy.setRequestBody({ - name: `my-auth-profile-for-${LOCAL.myGateway.gatewayId}`, - description: 'Auth connection to my IdP', - flow: 'client-credentials', - clientAuthenticator: 'client-secret', - mode: 'auto', - inheritFrom: 'Sample Shared IdP', - }) - cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/issuers`, 'PUT').then( - ({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - cy.log(JSON.stringify(body)) - } - ) - }) - - it('GET /gateways/{gatewayId}/issuers', () => { - cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/issuers`, 'GET').then( - ({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - cy.log(JSON.stringify(body, null, 2)) - expect(body.length).to.be.equal(1) - - const issuer = body[0] - - expect(issuer.name).to.be.equal( - `my-auth-profile-for-${LOCAL.myGateway.gatewayId}` - ) - expect(issuer.environmentDetails[0].environment).to.be.equal('test') - expect(issuer.environmentDetails[0].issuerUrl).to.be.equal( - Cypress.env('OIDC_ISSUER') - ) - expect(issuer.environmentDetails[0].clientId).to.be.equal( - `ap-my-auth-profile-for-${LOCAL.myGateway.gatewayId}-test` - ) - } - ) - }) -}) - -describe('Identifiers', () => { - it('GET /identifiers/application', () => { - cy.callAPI('ds/api/v3/identifiers/application', 'GET').then( - ({ apiRes: { status, body } }: any) => { - cy.log(`ID ${body}`) - expect(status).to.be.equal(200) - } - ) - }) - - it('GET /identifiers/product', () => { - cy.callAPI('ds/api/v3/identifiers/product', 'GET').then( - ({ apiRes: { status, body } }: any) => { - cy.log(`ID ${body}`) - expect(status).to.be.equal(200) - } - ) - }) - - it('GET /identifiers/environment', () => { - cy.callAPI('ds/api/v3/identifiers/environment', 'GET').then( - ({ apiRes: { status, body } }: any) => { - cy.log(`ID ${body}`) - expect(status).to.be.equal(200) - } - ) - }) - - it('GET /identifiers/gateway', () => { - cy.callAPI('ds/api/v3/identifiers/gateway', 'GET').then( - ({ apiRes: { status, body } }: any) => { - cy.log(`ID ${body}`) - expect(status).to.be.equal(200) - } - ) - }) -}) From 455903ed9b8e492aeef08d9da23e248775908301 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 6 Jun 2024 11:17:50 -0700 Subject: [PATCH 048/191] add endpoint availability check --- src/controllers/v3/EndpointsController.ts | 70 +++++++++++++++++++++++ src/controllers/v3/openapi.yaml | 19 ++++++ src/controllers/v3/routes.ts | 31 ++++++++++ 3 files changed, 120 insertions(+) create mode 100644 src/controllers/v3/EndpointsController.ts diff --git a/src/controllers/v3/EndpointsController.ts b/src/controllers/v3/EndpointsController.ts new file mode 100644 index 000000000..3ede29ee1 --- /dev/null +++ b/src/controllers/v3/EndpointsController.ts @@ -0,0 +1,70 @@ +import { + Controller, + OperationId, + Get, + Route, + Tags, + Query, + Request, +} from 'tsoa'; +import { KeystoneService } from '../ioc/keystoneInjector'; +import { inject, injectable } from 'tsyringe'; +import { getRecords } from '../../batch/feed-worker'; +import { GatewayRoute } from './types'; + +@injectable() +@Route('/routes') +@Tags('Service Routes') +export class EndpointsController extends Controller { + private keystone: KeystoneService; + constructor(@inject('KeystoneService') private _keystone: KeystoneService) { + super(); + this.keystone = _keystone; + } + + @Get('availability') + @OperationId('check-availability') + public async check( + @Query() serviceName: string, + @Request() request: any + ): Promise { + const ctx = this.keystone.sudo(); + const records = await getRecords( + ctx, + 'GatewayRoute', + 'allGatewayRoutes', + [] + ); + + let counter = 0; + let matchHostList; + do { + counter++; + matchHostList = this.getMatchHostList( + counter == 1 ? serviceName : `${serviceName}-${counter}` + ); + } while (this.isTaken(records, matchHostList)); + + return { + available: counter == 1 ? 'yes' : 'no', + suggestion: matchHostList[0], + }; + } + + private getMatchHostList(serviceName: string): string[] { + return [ + `${serviceName}.api.gov.bc.ca`, + `${serviceName}-api-gov-bc-ca.dev.api.gov.bc.ca`, + `${serviceName}-api-gov-bc-ca.test.api.gov.bc.ca`, + ]; + } + + private isTaken(records: any[], matchHosts: string[]): boolean { + return ( + records.filter( + (r: GatewayRoute) => + r.hosts.filter((h: string) => matchHosts.indexOf(h) >= 0).length > 0 + ).length > 0 + ); + } +} diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 56fc2cc74..190129ef0 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -797,6 +797,25 @@ paths: required: true schema: type: string + /routes/availability: + get: + operationId: check-availability + responses: + '200': + description: Ok + content: + application/json: + schema: {} + tags: + - 'Service Routes' + security: [] + parameters: + - + in: query + name: serviceName + required: true + schema: + type: string /gateways/report: get: operationId: report diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index 1af730ae5..2ca37bb37 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -9,6 +9,8 @@ import { DirectoryController } from './DirectoryController'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa import { DatasetController } from './DatasetController'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { EndpointsController } from './EndpointsController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa import { NamespaceController } from './GatewayController'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa import { NamespaceDirectoryController } from './GatewayDirectoryController'; @@ -617,6 +619,35 @@ export function RegisterRoutes(app: express.Router) { } }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/routes/availability', + + async function EndpointsController_check(request: any, response: any, next: any) { + const args = { + serviceName: {"in":"query","name":"serviceName","required":true,"dataType":"string"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(EndpointsController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.check.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.get('/ds/api/v3/gateways/report', authenticateMiddleware([{"jwt":[]}]), From 71a339771445d67691efb95999a843c7f90061c0 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 6 Jun 2024 11:21:30 -0700 Subject: [PATCH 049/191] make available a boolean --- src/controllers/v3/EndpointsController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/v3/EndpointsController.ts b/src/controllers/v3/EndpointsController.ts index 3ede29ee1..469bc6f67 100644 --- a/src/controllers/v3/EndpointsController.ts +++ b/src/controllers/v3/EndpointsController.ts @@ -46,7 +46,7 @@ export class EndpointsController extends Controller { } while (this.isTaken(records, matchHostList)); return { - available: counter == 1 ? 'yes' : 'no', + available: counter == 1, suggestion: matchHostList[0], }; } From f322e01e5a0a3db2ee52a0ff9685185c42cc28ac Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 6 Jun 2024 12:00:46 -0700 Subject: [PATCH 050/191] upd route availability --- src/controllers/v3/EndpointsController.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/controllers/v3/EndpointsController.ts b/src/controllers/v3/EndpointsController.ts index 469bc6f67..f2120907a 100644 --- a/src/controllers/v3/EndpointsController.ts +++ b/src/controllers/v3/EndpointsController.ts @@ -43,20 +43,26 @@ export class EndpointsController extends Controller { matchHostList = this.getMatchHostList( counter == 1 ? serviceName : `${serviceName}-${counter}` ); - } while (this.isTaken(records, matchHostList)); + } while (this.isTaken(records, matchHostList.hosts)); return { available: counter == 1, - suggestion: matchHostList[0], + name: matchHostList.serviceName, + host: matchHostList.hosts[0], }; } - private getMatchHostList(serviceName: string): string[] { - return [ - `${serviceName}.api.gov.bc.ca`, - `${serviceName}-api-gov-bc-ca.dev.api.gov.bc.ca`, - `${serviceName}-api-gov-bc-ca.test.api.gov.bc.ca`, - ]; + private getMatchHostList( + serviceName: string + ): { serviceName: string; hosts: string[] } { + return { + serviceName, + hosts: [ + `${serviceName}.api.gov.bc.ca`, + `${serviceName}-api-gov-bc-ca.dev.api.gov.bc.ca`, + `${serviceName}-api-gov-bc-ca.test.api.gov.bc.ca`, + ], + }; } private isTaken(records: any[], matchHosts: string[]): boolean { From 7553ab7c48f439f165a83ce676909271aa835485 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Tue, 11 Jun 2024 11:19:22 -0700 Subject: [PATCH 051/191] replace gatewayId in create-gateway call --- src/controllers/v3/GatewayController.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/v3/GatewayController.ts b/src/controllers/v3/GatewayController.ts index 863ea21bb..ca4518c02 100644 --- a/src/controllers/v3/GatewayController.ts +++ b/src/controllers/v3/GatewayController.ts @@ -15,6 +15,7 @@ import { import { ValidateError, FieldErrors } from 'tsoa'; import { KeystoneService } from '../ioc/keystoneInjector'; import { inject, injectable } from 'tsyringe'; +import { replaceKey } from '../../batch/feed-worker'; import { gql } from 'graphql-request'; import { WorkbookService } from '../../services/report/workbook.service'; import { Namespace, NamespaceInput } from '../../services/keystone/types'; @@ -155,10 +156,11 @@ export class NamespaceController extends Controller { @Body() vars: Gateway ): Promise { logger.debug('Input %j', vars); + const modifiedVars = replaceKey(vars, 'gatewayId', 'name'); const result = await this.keystone.executeGraphQL({ context: this.keystone.createContext(request), query: createNS, - variables: vars, + variables: modifiedVars, }); logger.debug('Result %j', result); if (result.errors) { From 90d672087f834fbfc2ea1241c99689a2eb2184c1 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 11 Jun 2024 22:28:05 -0700 Subject: [PATCH 052/191] v3 upds generate display name and dataset resources contacts --- e2e/cypress/support/prep-commands.ts | 2 + .../tests/19-api-v3/01-api-directory.ts | 357 +++++++++++------- e2e/cypress/tests/19-api-v3/03-gateways.ts | 157 +++++--- e2e/cypress/tests/19-api-v3/04-products.ts | 62 +-- e2e/cypress/tests/19-api-v3/05-issuers.ts | 70 ++-- src/auth/auth-tsoa.ts | 2 + src/batch/data-rules.js | 48 ++- src/controllers/v2/openapi.yaml | 45 ++- src/controllers/v2/routes.ts | 29 +- src/controllers/v2/types.ts | 31 +- src/controllers/v3/DatasetController.ts | 2 +- src/controllers/v3/EndpointsController.ts | 4 +- src/controllers/v3/GatewayController.ts | 36 +- .../v3/GatewayDirectoryController.ts | 4 +- .../v3/GatewayServicesController.ts | 2 +- src/controllers/v3/OrgDatasetController.ts | 2 +- src/controllers/v3/OrganizationController.ts | 2 +- src/controllers/v3/openapi.yaml | 95 ++++- src/controllers/v3/routes.ts | 73 +++- src/controllers/v3/t.js | 33 ++ src/controllers/v3/types.ts | 31 +- src/lists/extensions/Namespace.ts | 4 +- src/nextapp/.env.local | 2 +- src/services/keycloak/namespace-details.ts | 9 + src/tsoa-v3.json | 4 + 25 files changed, 805 insertions(+), 301 deletions(-) create mode 100644 src/controllers/v3/t.js diff --git a/e2e/cypress/support/prep-commands.ts b/e2e/cypress/support/prep-commands.ts index c7f8562bd..077c837d6 100644 --- a/e2e/cypress/support/prep-commands.ts +++ b/e2e/cypress/support/prep-commands.ts @@ -124,6 +124,8 @@ Cypress.Commands.add('buildOrgGatewayDatasetAndProduct', (): Cypress.Chainable { }) }) - it('PUT /organizations/{org}/datasets', () => { - const { org, gateway, dataset, datasetId, product } = workingData - cy.callAPI(`ds/api/v3/directory/${datasetId}`, 'GET').then( - ({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) + describe('API Directory (Public)', () => { + it('PUT /organizations/{org}/datasets (resources/contacts)', () => { + const { org, gateway, dataset, datasetId, product } = workingData - const match = { - name: dataset.name, - title: 'A title about my dataset', - notes: 'Some notes', - license_title: 'Open Government Licence - British Columbia', - security_class: 'PUBLIC', - view_audience: 'Public', - tags: ['tag1', 'tag2'], - record_publish_date: '2017-09-05', - isInCatalog: false, - organization: { - name: org.name, - title: 'Some good title about kittens', - }, - organizationUnit: { - name: org.orgUnits[0].name, - title: 'Division of fun toys to play', - }, - products: [ - { - name: `my-product-on-${gateway.gatewayId}`, - environments: [{ name: 'dev', active: true, flow: 'public', services: [] }], - }, - ], - } - delete body.products[0].id - expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) - } - ) - }) - - it('GET /directory', () => { - cy.callAPI('ds/api/v3/directory', 'GET').then(({ apiRes: { status, body } }: any) => { - cy.log(`Directory ${JSON.stringify(body, null, 4)}`) - expect(status).to.be.equal(200) - expect(body.length).to.be.greaterThan(0) - }) - }) - - it('GET /directory/{id}', () => { - cy.callAPI('ds/api/v3/directory', 'GET').then(({ apiRes: { status, body } }: any) => { - cy.log(`Directory ${JSON.stringify(body, null, 4)}`) - expect(status).to.be.equal(200) - }) - }) -}) - -describe('API Directory (Gateway Management)', () => { - let workingData: any - - before(() => { - cy.buildOrgGatewayDatasetAndProduct().then((data) => { - workingData = data - }) - }) - - it('GET /gateways/{gatewayId}/datasets/{name}', () => { - const { org, gateway, dataset, datasetId, product } = workingData - cy.callAPI( - `ds/api/v3/gateways/${gateway.gatewayId}/datasets/${dataset.name}`, - 'GET' - ).then(({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) - - const match = { - name: dataset.name, + const payload = { + name: `org-dataset-${datasetId}-res`, license_title: 'Open Government Licence - British Columbia', security_class: 'PUBLIC', view_audience: 'Public', @@ -86,45 +20,106 @@ describe('API Directory (Gateway Management)', () => { record_publish_date: '2017-09-05', notes: 'Some notes', title: 'A title about my dataset', - isInCatalog: false, - isDraft: true, tags: ['tag1', 'tag2'], - organization: { - name: org.name, - title: 'Some good title about kittens', - tags: [], - description: 'Some good description about kittens', - }, - organizationUnit: { - name: org.orgUnits[0].name, - title: 'Division of fun toys to play', - tags: [], - description: 'Some good description about how we manage our toys', - }, + organization: org.name, + organizationUnit: org.orgUnits[0].name, + resources: [ + { + name: 'API Doc', + url: 'https://raw.githubusercontent.com/bcgov/api-specs/master/bcdc/bcdc.json', + format: 'openapi-json', + }, + ], + contacts: [ + { + name: 'Joe Smith', + email: 'joe@gov.bc.ca', + role: 'pointOfContact', + }, + ], } + cy.setRequestBody(payload) + cy.callAPI(`ds/api/v3/organizations/${org.name}/datasets`, 'PUT').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) - delete body.id + cy.callAPI( + `ds/api/v3/organizations/${org.name}/datasets/${payload.name}`, + 'GET' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) - expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + const match = { + name: payload.name, + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + download_audience: 'Public', + record_publish_date: '2017-09-05', + notes: 'Some notes', + title: 'A title about my dataset', + isInCatalog: false, + isDraft: true, + contacts: [ + { + role: 'pointOfContact', + name: 'Joe Smith', + email: 'joe@gov.bc.ca', + }, + ], + resources: [ + { + name: 'API Doc', + url: 'https://raw.githubusercontent.com/bcgov/api-specs/master/bcdc/bcdc.json', + format: 'openapi-json', + }, + ], + tags: ['tag1', 'tag2'], + organization: { + name: org.name, + title: 'Some good title about kittens', + tags: [], + description: 'Some good description about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + tags: [], + description: 'Some good description about how we manage our toys', + }, + } + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) + } + ) }) - }) - it('GET /gateways/{gatewayId}/directory', () => { - const { org, gateway, dataset, datasetId, product } = workingData - cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/directory`, 'GET').then( - ({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) + it('GET /directory', () => { + cy.callAPI('ds/api/v3/directory', 'GET').then( + ({ apiRes: { status, body } }: any) => { + cy.log(`Directory ${JSON.stringify(body, null, 4)}`) + expect(status).to.be.equal(200) + expect(body.length).to.be.greaterThan(0) + } + ) + }) - const match = [ - { + it('GET /directory/{datasetId}', () => { + const { org, gateway, dataset, datasetId, product } = workingData + cy.callAPI(`ds/api/v3/directory/${datasetId}`, 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { name: dataset.name, title: 'A title about my dataset', notes: 'Some notes', license_title: 'Open Government Licence - British Columbia', - view_audience: 'Public', security_class: 'PUBLIC', - record_publish_date: '2017-09-05', + view_audience: 'Public', tags: ['tag1', 'tag2'], + record_publish_date: '2017-09-05', + isInCatalog: false, organization: { name: org.name, title: 'Some good title about kittens', @@ -135,59 +130,143 @@ describe('API Directory (Gateway Management)', () => { }, products: [ { - name: product.name, - environments: [{ name: 'dev', active: true, flow: 'public' }], + name: `my-product-on-${gateway.gatewayId}`, + environments: [ + { name: 'dev', active: true, flow: 'public', services: [] }, + ], }, ], + } + delete body.products[0].id + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + }) + + describe('API Directory (Gateway Management)', () => { + it('GET /gateways/{gatewayId}/datasets/{name}', () => { + const { org, gateway, dataset, datasetId, product } = workingData + cy.callAPI( + `ds/api/v3/gateways/${gateway.gatewayId}/datasets/${dataset.name}`, + 'GET' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: dataset.name, + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + download_audience: 'Public', + record_publish_date: '2017-09-05', + notes: 'Some notes', + title: 'A title about my dataset', + isInCatalog: false, + isDraft: true, + contacts: [], + resources: [], + tags: ['tag1', 'tag2'], + organization: { + name: org.name, + title: 'Some good title about kittens', + tags: [], + description: 'Some good description about kittens', }, - ] + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + tags: [], + description: 'Some good description about how we manage our toys', + }, + } - delete body[0].products[0].id - delete body[0].id + delete body.id expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) - } - ) - }) + }) + }) - it('GET /gateways/{gatewayId}/directory/{id}', () => { - const { org, gateway, dataset, datasetId, product } = workingData - cy.callAPI( - `ds/api/v3/gateways/${gateway.gatewayId}/directory/${datasetId}`, - 'GET' - ).then(({ apiRes: { status, body } }: any) => { - expect(status).to.be.equal(200) + it('GET /gateways/{gatewayId}/directory', () => { + const { org, gateway, dataset, datasetId, product } = workingData + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/directory`, 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) - const match = { - name: dataset.name, - title: 'A title about my dataset', - notes: 'Some notes', - license_title: 'Open Government Licence - British Columbia', - security_class: 'PUBLIC', - view_audience: 'Public', - tags: ['tag1', 'tag2'], - record_publish_date: '2017-09-05', - isInCatalog: false, - organization: { - name: org.name, - title: 'Some good title about kittens', - }, - organizationUnit: { - name: org.orgUnits[0].name, - title: 'Division of fun toys to play', - }, - products: [ - { - name: product.name, - environments: [{ name: 'dev', active: true, flow: 'public', services: [] }], + const match = [ + { + name: dataset.name, + title: 'A title about my dataset', + notes: 'Some notes', + license_title: 'Open Government Licence - British Columbia', + view_audience: 'Public', + security_class: 'PUBLIC', + record_publish_date: '2017-09-05', + tags: ['tag1', 'tag2'], + organization: { + name: org.name, + title: 'Some good title about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + }, + products: [ + { + name: product.name, + environments: [{ name: 'dev', active: true, flow: 'public' }], + }, + ], + }, + ] + + delete body[0].products[0].id + delete body[0].id + + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('GET /gateways/{gatewayId}/directory/{id}', () => { + const { org, gateway, dataset, datasetId, product } = workingData + cy.callAPI( + `ds/api/v3/gateways/${gateway.gatewayId}/directory/${datasetId}`, + 'GET' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: dataset.name, + title: 'A title about my dataset', + notes: 'Some notes', + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + tags: ['tag1', 'tag2'], + record_publish_date: '2017-09-05', + isInCatalog: false, + organization: { + name: org.name, + title: 'Some good title about kittens', }, - ], - } + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + }, + products: [ + { + name: product.name, + environments: [{ name: 'dev', active: true, flow: 'public', services: [] }], + }, + ], + } - delete body.products[0].id - delete body.id + delete body.products[0].id + delete body.id - expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) }) }) }) diff --git a/e2e/cypress/tests/19-api-v3/03-gateways.ts b/e2e/cypress/tests/19-api-v3/03-gateways.ts index 8fb858056..0b377ce0c 100644 --- a/e2e/cypress/tests/19-api-v3/03-gateways.ts +++ b/e2e/cypress/tests/19-api-v3/03-gateways.ts @@ -7,18 +7,83 @@ describe('Gateways', () => { }) }) - it('POST /gateways', () => { - const payload = { - displayName: 'My ABC Gateway', - } - cy.setRequestBody(payload) - cy.callAPI('ds/api/v3/gateways', 'POST').then(({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - cy.log(JSON.stringify(body, null, 2)) - expect(body.displayName).to.be.equal(payload.displayName) + describe('Happy Paths', () => { + it('POST /gateways', () => { + const payload = { + displayName: 'My ABC Gateway', + } + cy.setRequestBody(payload) + cy.callAPI('ds/api/v3/gateways', 'POST').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.displayName).to.be.equal(payload.displayName) + + const gateway = body - const gateway = body + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.displayName).to.be.equal(gateway.displayName) + } + ) + + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/activity`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.length).to.be.equal(1) + expect(body[0].message).to.be.equal('{actor} created {ns} namespace') + expect(body[0].params.ns).to.be.equal(gateway.gatewayId) + } + ) + } + ) + }) + + it('POST /gateways (with gatewayId)', () => { + const { v4: uuidv4 } = require('uuid') + const customId = uuidv4().replace(/-/g, '').toLowerCase().substring(0, 3) + + const payload = { + gatewayId: `custom-${customId}-gw`, + displayName: 'My ABC Gateway', + } + cy.log(JSON.stringify(payload)) + cy.setRequestBody(payload) + cy.callAPI('ds/api/v3/gateways', 'POST').then( + ({ apiRes: { body, status } }: any) => { + cy.log(body) + expect(status).to.be.equal(200) + expect(body.gatewayId).to.be.equal(payload.gatewayId) + expect(body.displayName).to.be.equal(payload.displayName) + } + ) + }) + it('POST /gateways (no displayname)', () => { + const { v4: uuidv4 } = require('uuid') + const customId = uuidv4().replace(/-/g, '').toLowerCase().substring(0, 10) + const payload = { + gatewayId: `a${customId}a`, + } + cy.log(JSON.stringify(payload)) + cy.setRequestBody(payload) + cy.callAPI('ds/api/v3/gateways', 'POST').then( + ({ apiRes: { body, status } }: any) => { + const match = { + gatewayId: payload.gatewayId, + displayName: "janis's Gateway", + } + expect(status).to.be.equal(200) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('GET /gateways/{gatewayId}', () => { + const { gateway } = workingData cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}`, 'GET').then( ({ apiRes: { body, status } }: any) => { expect(status).to.be.equal(200) @@ -26,50 +91,54 @@ describe('Gateways', () => { expect(body.displayName).to.be.equal(gateway.displayName) } ) + }) + it('GET /gateways/{gatewayId}/activity', () => { + const { gateway } = workingData cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/activity`, 'GET').then( ({ apiRes: { body, status } }: any) => { expect(status).to.be.equal(200) cy.log(JSON.stringify(body, null, 2)) - expect(body.length).to.be.equal(1) - expect(body[0].message).to.be.equal('{actor} created {ns} namespace') - expect(body[0].params.ns).to.be.equal(gateway.gatewayId) + expect(body.length).to.be.equal(3) + expect(body[2].message).to.be.equal('{actor} created {ns} namespace') + expect(body[2].params.ns).to.be.equal(gateway.gatewayId) } ) }) - }) - it('GET /gateways/{gatewayId}', () => { - const { gateway } = workingData - cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}`, 'GET').then( - ({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - cy.log(JSON.stringify(body, null, 2)) - expect(body.displayName).to.be.equal(gateway.displayName) - } - ) + // it('DELETE /gateways/{gatewayId}', () => { + // const { gateway } = workingData + // cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}`, 'DELETE').then( + // ({ apiRes: { body, status } }: any) => { + // expect(status).to.be.equal(200) + // cy.log(JSON.stringify(body, null, 2)) + // } + // ) + // }) }) - - it('GET /gateways/{gatewayId}/activity', () => { - const { gateway } = workingData - cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/activity`, 'GET').then( - ({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - cy.log(JSON.stringify(body, null, 2)) - expect(body.length).to.be.equal(3) - expect(body[2].message).to.be.equal('{actor} created {ns} namespace') - expect(body[2].params.ns).to.be.equal(gateway.gatewayId) + describe('Error Paths', () => { + it('POST /gateways (bad gatewayId)', () => { + const payload = { + gatewayId: `CAP-LETTERS`, + displayName: 'My ABC Gateway', } - ) + cy.log(JSON.stringify(payload)) + cy.setRequestBody(payload) + cy.callAPI('ds/api/v3/gateways', 'POST').then( + ({ apiRes: { body, status } }: any) => { + const match = { + message: 'Validation Failed', + details: { + d0: { + message: + 'Namespace name must be between 5 and 15 alpha-numeric lowercase characters and start and end with an alphabet.', + }, + }, + } + expect(status).to.be.equal(422) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) }) - - // it('DELETE /gateways/{gatewayId}', () => { - // const { gateway } = workingData - // cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}`, 'DELETE').then( - // ({ apiRes: { body, status } }: any) => { - // expect(status).to.be.equal(200) - // cy.log(JSON.stringify(body, null, 2)) - // } - // ) - // }) }) diff --git a/e2e/cypress/tests/19-api-v3/04-products.ts b/e2e/cypress/tests/19-api-v3/04-products.ts index e4338cfae..3815d68a2 100644 --- a/e2e/cypress/tests/19-api-v3/04-products.ts +++ b/e2e/cypress/tests/19-api-v3/04-products.ts @@ -7,37 +7,39 @@ describe('Products', () => { }) }) - it('PUT /gateways/{gatewayId}/products', () => { - const { gateway } = workingData - cy.setRequestBody({ - name: `my-product-on-${gateway.gatewayId}`, - environments: [ - { - name: 'dev', - active: false, - approval: false, - flow: 'public', - }, - ], + describe('Happy Paths', () => { + it('PUT /gateways/{gatewayId}/products', () => { + const { gateway } = workingData + cy.setRequestBody({ + name: `my-product-on-${gateway.gatewayId}`, + environments: [ + { + name: 'dev', + active: false, + approval: false, + flow: 'public', + }, + ], + }) + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/products`, 'PUT').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body)) + } + ) }) - cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/products`, 'PUT').then( - ({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - cy.log(JSON.stringify(body)) - } - ) - }) - it('GET /gateways/{gatewayId}/products', () => { - const { gateway } = workingData - cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/products`, 'GET').then( - ({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - cy.log(JSON.stringify(body, null, 2)) - expect(body.length).to.be.equal(1) - expect(body[0].name).to.be.equal(`my-product-on-${gateway.gatewayId}`) - expect(body[0].environments.length).to.be.equal(1) - } - ) + it('GET /gateways/{gatewayId}/products', () => { + const { gateway } = workingData + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/products`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.length).to.be.equal(1) + expect(body[0].name).to.be.equal(`my-product-on-${gateway.gatewayId}`) + expect(body[0].environments.length).to.be.equal(1) + } + ) + }) }) }) diff --git a/e2e/cypress/tests/19-api-v3/05-issuers.ts b/e2e/cypress/tests/19-api-v3/05-issuers.ts index 433ca3dd3..2e903b6fe 100644 --- a/e2e/cypress/tests/19-api-v3/05-issuers.ts +++ b/e2e/cypress/tests/19-api-v3/05-issuers.ts @@ -7,43 +7,45 @@ describe('Authorization Profiles', () => { }) }) - it('PUT /gateways/{gatewayId}/issuers', () => { - const { gateway } = workingData - cy.setRequestBody({ - name: `my-auth-profile-for-${gateway.gatewayId}`, - description: 'Auth connection to my IdP', - flow: 'client-credentials', - clientAuthenticator: 'client-secret', - mode: 'auto', - inheritFrom: 'Sample Shared IdP', + describe('Happy Paths', () => { + it('PUT /gateways/{gatewayId}/issuers', () => { + const { gateway } = workingData + cy.setRequestBody({ + name: `my-auth-profile-for-${gateway.gatewayId}`, + description: 'Auth connection to my IdP', + flow: 'client-credentials', + clientAuthenticator: 'client-secret', + mode: 'auto', + inheritFrom: 'Sample Shared IdP', + }) + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/issuers`, 'PUT').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body)) + } + ) }) - cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/issuers`, 'PUT').then( - ({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - cy.log(JSON.stringify(body)) - } - ) - }) - it('GET /gateways/{gatewayId}/issuers', () => { - const { gateway } = workingData - cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/issuers`, 'GET').then( - ({ apiRes: { body, status } }: any) => { - expect(status).to.be.equal(200) - cy.log(JSON.stringify(body, null, 2)) - expect(body.length).to.be.equal(1) + it('GET /gateways/{gatewayId}/issuers', () => { + const { gateway } = workingData + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/issuers`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.length).to.be.equal(1) - const issuer = body[0] + const issuer = body[0] - expect(issuer.name).to.be.equal(`my-auth-profile-for-${gateway.gatewayId}`) - expect(issuer.environmentDetails[0].environment).to.be.equal('test') - expect(issuer.environmentDetails[0].issuerUrl).to.be.equal( - Cypress.env('OIDC_ISSUER') - ) - expect(issuer.environmentDetails[0].clientId).to.be.equal( - `ap-my-auth-profile-for-${gateway.gatewayId}-test` - ) - } - ) + expect(issuer.name).to.be.equal(`my-auth-profile-for-${gateway.gatewayId}`) + expect(issuer.environmentDetails[0].environment).to.be.equal('test') + expect(issuer.environmentDetails[0].issuerUrl).to.be.equal( + Cypress.env('OIDC_ISSUER') + ) + expect(issuer.environmentDetails[0].clientId).to.be.equal( + `ap-my-auth-profile-for-${gateway.gatewayId}-test` + ) + } + ) + }) }) }) diff --git a/src/auth/auth-tsoa.ts b/src/auth/auth-tsoa.ts index ad837461c..69fc5bb92 100644 --- a/src/auth/auth-tsoa.ts +++ b/src/auth/auth-tsoa.ts @@ -57,6 +57,8 @@ export function expressAuthentication( resource = `org/${request.params.org}`; } else if ('gatewayId' in request.params) { resource = request.params.gatewayId; + } else if ('gatewayId' in request.query) { + resource = request.query.gatewayId; } else { // assume it is namespace-based protection resource = request.params.ns; diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index 6dea337a9..08fc326af 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -61,7 +61,7 @@ const metadata = { ], transformations: { tags: { name: 'toStringDefaultArray' }, - resources: { name: 'toString' }, + resources: { name: 'toStringDefaultArray' }, organization: { name: 'connectOne', key: 'organization.id', @@ -77,6 +77,16 @@ const metadata = { isInCatalog: { name: 'alwaysTrue' }, isDraft: { name: 'alwaysFalse' }, }, + validations: { + resources: { + type: 'entityArray', + entity: 'DatasetResource', + }, + contacts: { + type: 'entityArray', + entity: 'DatasetContact', + }, + }, }, DraftDataset: { entity: 'Dataset', @@ -101,6 +111,8 @@ const metadata = { ], transformations: { tags: { name: 'toStringDefaultArray' }, + resources: { name: 'toStringDefaultArray' }, + contacts: { name: 'toStringDefaultArray' }, organization: { name: 'connectOne', list: 'allOrganizations', @@ -152,8 +164,14 @@ const metadata = { }, isInCatalog: { type: 'boolean' }, isDraft: { type: 'boolean' }, - // contacts : TBD - // resources: TBD + resources: { + type: 'entityArray', + entity: 'DatasetResource', + }, + contacts: { + type: 'entityArray', + entity: 'DatasetContact', + }, }, example: { name: 'my_sample_dataset', @@ -708,6 +726,30 @@ const metadata = { sync: ['ref', 'type', 'blob'], transformations: {}, }, + DatasetContact: { + transient: true, + refKey: 'name', + sync: ['name', 'email', 'role'], + validations: { + role: { + type: 'enum', + values: ['pointOfContact'], + }, + }, + transformations: {}, + }, + DatasetResource: { + transient: true, + refKey: 'id', + sync: ['name', 'format', 'url'], + validations: { + format: { + type: 'enum', + values: ['openapi-json', 'json'], + }, + }, + transformations: {}, + }, }; module.exports.metadata = metadata; diff --git a/src/controllers/v2/openapi.yaml b/src/controllers/v2/openapi.yaml index 6696f64e2..50002bffb 100644 --- a/src/controllers/v2/openapi.yaml +++ b/src/controllers/v2/openapi.yaml @@ -71,6 +71,34 @@ components: tags: - tag1 - tag2 + DatasetContact: + properties: + name: + type: string + email: + type: string + role: + type: string + enum: + - pointOfContact + nullable: false + type: object + additionalProperties: false + DatasetResource: + properties: + id: + type: string + name: + type: string + format: + type: string + enum: + - openapi-json + - json + url: + type: string + type: object + additionalProperties: false OrganizationRefID: type: string OrganizationUnitRefID: @@ -100,7 +128,13 @@ components: isDraft: type: string contacts: - type: string + items: + $ref: '#/components/schemas/DatasetContact' + type: array + resources: + items: + $ref: '#/components/schemas/DatasetResource' + type: array extSource: type: string extRecordHash: @@ -109,7 +143,6 @@ components: items: type: string type: array - resources: {} organization: $ref: '#/components/schemas/OrganizationRefID' organizationUnit: @@ -161,9 +194,13 @@ components: isDraft: type: boolean contacts: - type: string + items: + $ref: '#/components/schemas/DatasetContact' + type: array resources: - type: string + items: + $ref: '#/components/schemas/DatasetResource' + type: array tags: items: type: string diff --git a/src/controllers/v2/routes.ts b/src/controllers/v2/routes.ts index 4d6561ced..d141d7077 100644 --- a/src/controllers/v2/routes.ts +++ b/src/controllers/v2/routes.ts @@ -73,6 +73,27 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DatasetContact": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string"}, + "email": {"dataType":"string"}, + "role": {"dataType":"enum","enums":["pointOfContact"]}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DatasetResource": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string"}, + "name": {"dataType":"string"}, + "format": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["openapi-json"]},{"dataType":"enum","enums":["json"]}]}, + "url": {"dataType":"string"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "OrganizationRefID": { "dataType": "refAlias", "type": {"dataType":"string","validators":{}}, @@ -97,11 +118,11 @@ const models: TsoaRoute.Models = { "title": {"dataType":"string"}, "isInCatalog": {"dataType":"string"}, "isDraft": {"dataType":"string"}, - "contacts": {"dataType":"string"}, + "contacts": {"dataType":"array","array":{"dataType":"refObject","ref":"DatasetContact"}}, + "resources": {"dataType":"array","array":{"dataType":"refObject","ref":"DatasetResource"}}, "extSource": {"dataType":"string"}, "extRecordHash": {"dataType":"string"}, "tags": {"dataType":"array","array":{"dataType":"string"}}, - "resources": {"dataType":"any"}, "organization": {"ref":"OrganizationRefID"}, "organizationUnit": {"ref":"OrganizationUnitRefID"}, }, @@ -121,8 +142,8 @@ const models: TsoaRoute.Models = { "title": {"dataType":"string"}, "isInCatalog": {"dataType":"boolean"}, "isDraft": {"dataType":"boolean"}, - "contacts": {"dataType":"string"}, - "resources": {"dataType":"string"}, + "contacts": {"dataType":"array","array":{"dataType":"refObject","ref":"DatasetContact"}}, + "resources": {"dataType":"array","array":{"dataType":"refObject","ref":"DatasetResource"}}, "tags": {"dataType":"array","array":{"dataType":"string"}}, "organization": {"ref":"OrganizationRefID"}, "organizationUnit": {"ref":"OrganizationUnitRefID"}, diff --git a/src/controllers/v2/types.ts b/src/controllers/v2/types.ts index f610a40cb..c5909c0cc 100644 --- a/src/controllers/v2/types.ts +++ b/src/controllers/v2/types.ts @@ -56,11 +56,11 @@ export interface Dataset { title?: string; isInCatalog?: string; isDraft?: string; - contacts?: string; + contacts?: DatasetContact[]; + resources?: DatasetResource[]; extSource?: string; extRecordHash?: string; tags?: string[]; - resources?: any; // toString organization?: OrganizationRefID; organizationUnit?: OrganizationUnitRefID; } @@ -96,8 +96,8 @@ export interface DraftDataset { title?: string; isInCatalog?: boolean; isDraft?: boolean; - contacts?: string; - resources?: string; + contacts?: DatasetContact[]; + resources?: DatasetResource[]; tags?: string[]; organization?: OrganizationRefID; organizationUnit?: OrganizationUnitRefID; @@ -513,6 +513,29 @@ export interface Blob { blob?: string; } + +/** + * @tsoaModel + * + */ +export interface DatasetContact { + name?: string; // Primary Key + email?: string; + role?: "pointOfContact"; +} + + +/** + * @tsoaModel + * + */ +export interface DatasetResource { + id?: string; // Primary Key + name?: string; + format?: "openapi-json" | "json"; + url?: string; +} + /** * @tsoaModel */ diff --git a/src/controllers/v3/DatasetController.ts b/src/controllers/v3/DatasetController.ts index c9434d52c..57ff6a119 100644 --- a/src/controllers/v3/DatasetController.ts +++ b/src/controllers/v3/DatasetController.ts @@ -30,7 +30,7 @@ import { Product } from '@/services/keystone/types'; @injectable() @Route('/gateways/{gatewayId}/datasets') @Security('jwt', ['Namespace.Manage']) -@Tags('API Directory') +@Tags('API Directory (Administration)') export class DatasetController extends Controller { private keystone: KeystoneService; constructor(@inject('KeystoneService') private _keystone: KeystoneService) { diff --git a/src/controllers/v3/EndpointsController.ts b/src/controllers/v3/EndpointsController.ts index f2120907a..fa44fc6e9 100644 --- a/src/controllers/v3/EndpointsController.ts +++ b/src/controllers/v3/EndpointsController.ts @@ -6,10 +6,12 @@ import { Tags, Query, Request, + Security, + Path, } from 'tsoa'; import { KeystoneService } from '../ioc/keystoneInjector'; import { inject, injectable } from 'tsyringe'; -import { getRecords } from '../../batch/feed-worker'; +import { getRecords, removeEmpty } from '../../batch/feed-worker'; import { GatewayRoute } from './types'; @injectable() diff --git a/src/controllers/v3/GatewayController.ts b/src/controllers/v3/GatewayController.ts index ca4518c02..fccdece92 100644 --- a/src/controllers/v3/GatewayController.ts +++ b/src/controllers/v3/GatewayController.ts @@ -15,7 +15,7 @@ import { import { ValidateError, FieldErrors } from 'tsoa'; import { KeystoneService } from '../ioc/keystoneInjector'; import { inject, injectable } from 'tsyringe'; -import { replaceKey } from '../../batch/feed-worker'; +import { getRecords, replaceKey } from '../../batch/feed-worker'; import { gql } from 'graphql-request'; import { WorkbookService } from '../../services/report/workbook.service'; import { Namespace, NamespaceInput } from '../../services/keystone/types'; @@ -32,7 +32,7 @@ import { import { strict as assert } from 'assert'; import { Logger } from '../../logger'; -import { Activity, Gateway } from './types'; +import { Activity, Gateway, GatewayRoute } from './types'; import { getActivity } from '../../services/keystone/activity'; import { transformActivity } from '../../services/workflow'; import { ActivityDetail } from './types-extra'; @@ -243,6 +243,38 @@ export class NamespaceController extends Controller { .map((o) => parseJsonString(o, ['context'])) .map((o) => parseBlobString(o)); } + + /** + * Get a summary of your endpoints + * > `Required Scope:` Namespace.Manage + * + * @summary Get endpoints + */ + @Get('/{gatewayId}/links') + @OperationId('get-gateway-links') + @Security('jwt', ['Namespace.Manage']) + public async get( + @Path() gatewayId: string, + @Request() request: any + ): Promise<{ host: string }[]> { + const ctx = this.keystone.createContext(request); + const records = await getRecords( + ctx, + 'GatewayRoute', + 'allGatewayRoutesByNamespace', + [] + ); + + const endpoints: string[] = []; + records.forEach((r: GatewayRoute) => + r.hosts.forEach((h: string) => endpoints.push(h)) + ); + + return [...new Set(endpoints)].map((host) => ({ + type: 'route-host', + host: `https://${host}`, + })); + } } const list = gql` diff --git a/src/controllers/v3/GatewayDirectoryController.ts b/src/controllers/v3/GatewayDirectoryController.ts index 48c14b9d3..a5de7ce90 100644 --- a/src/controllers/v3/GatewayDirectoryController.ts +++ b/src/controllers/v3/GatewayDirectoryController.ts @@ -17,8 +17,8 @@ import { strict as assert } from 'assert'; @injectable() @Route('/gateways/{gatewayId}/directory') @Security('jwt', ['Namespace.Manage']) -@Tags('API Directory') -export class NamespaceDirectoryController extends Controller { +@Tags('API Directory (Administration)') +export class GatewayDirectoryController extends Controller { private keystone: KeystoneService; constructor(@inject('KeystoneService') private _keystone: KeystoneService) { super(); diff --git a/src/controllers/v3/GatewayServicesController.ts b/src/controllers/v3/GatewayServicesController.ts index 927087567..801e5837b 100644 --- a/src/controllers/v3/GatewayServicesController.ts +++ b/src/controllers/v3/GatewayServicesController.ts @@ -54,7 +54,7 @@ export class GatewayController extends Controller { @Get() @OperationId('get-gateway-routes') @Security('jwt', ['Namespace.Manage']) - public async get( + public async getServices( @Path() gatewayId: string, @Request() request: any ): Promise { diff --git a/src/controllers/v3/OrgDatasetController.ts b/src/controllers/v3/OrgDatasetController.ts index 698ea58c4..80e9b3219 100644 --- a/src/controllers/v3/OrgDatasetController.ts +++ b/src/controllers/v3/OrgDatasetController.ts @@ -28,7 +28,7 @@ import { Dataset, DraftDataset } from './types'; @injectable() @Route('/organizations') -@Tags('API Directory') +@Tags('API Directory (Administration)') export class OrgDatasetController extends Controller { private keystone: KeystoneService; constructor(@inject('KeystoneService') private _keystone: KeystoneService) { diff --git a/src/controllers/v3/OrganizationController.ts b/src/controllers/v3/OrganizationController.ts index 6f60c5ade..9d717ffef 100644 --- a/src/controllers/v3/OrganizationController.ts +++ b/src/controllers/v3/OrganizationController.ts @@ -266,7 +266,7 @@ export class OrganizationController extends Controller { /** * > `Required Scope:` Namespace.Assign * - * @summary Get Namespace Activity for gateways associated with this Organization Unit + * @summary Get administration activity for gateways associated with this Organization Unit * @param orgUnit * @param first * @param skip diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 190129ef0..64a12a4ec 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -5,6 +5,34 @@ components: requestBodies: {} responses: {} schemas: + DatasetContact: + properties: + name: + type: string + email: + type: string + role: + type: string + enum: + - pointOfContact + nullable: false + type: object + additionalProperties: false + DatasetResource: + properties: + id: + type: string + name: + type: string + format: + type: string + enum: + - openapi-json + - json + url: + type: string + type: object + additionalProperties: false OrganizationRefID: type: string OrganizationUnitRefID: @@ -34,7 +62,13 @@ components: isDraft: type: string contacts: - type: string + items: + $ref: '#/components/schemas/DatasetContact' + type: array + resources: + items: + $ref: '#/components/schemas/DatasetResource' + type: array extSource: type: string extRecordHash: @@ -43,7 +77,6 @@ components: items: type: string type: array - resources: {} organization: $ref: '#/components/schemas/OrganizationRefID' organizationUnit: @@ -117,9 +150,13 @@ components: isDraft: type: boolean contacts: - type: string + items: + $ref: '#/components/schemas/DatasetContact' + type: array resources: - type: string + items: + $ref: '#/components/schemas/DatasetResource' + type: array tags: items: type: string @@ -600,7 +637,7 @@ paths: description: "Get metadata about Datasets that are available by API for this organization\n> `Required Scope:` Dataset.Manage" summary: 'Get Organization Datasets' tags: - - 'API Directory' + - 'API Directory (Administration)' security: - jwt: @@ -624,7 +661,7 @@ paths: description: "Manage metadata about Datasets that are available by API for this organization\n> `Required Scope:` Dataset.Manage" summary: 'Manage Organization Datasets' tags: - - 'API Directory' + - 'API Directory (Administration)' security: - jwt: @@ -655,7 +692,7 @@ paths: description: "Delete a Dataset\n> `Required Scope:` Dataset.Manage" summary: 'Delete a dataset' tags: - - 'API Directory' + - 'API Directory (Administration)' security: - jwt: @@ -685,7 +722,7 @@ paths: description: "Get metadata about a Dataset that are available by API for this organization\n> `Required Scope:` Dataset.Manage" summary: 'Get Organization Dataset' tags: - - 'API Directory' + - 'API Directory (Administration)' security: - jwt: @@ -748,7 +785,7 @@ paths: description: "Update metadata about a Dataset\n> `Required Scope:` Namespace.Manage" summary: 'Update Dataset' tags: - - 'API Directory' + - 'API Directory (Administration)' security: - jwt: @@ -779,7 +816,7 @@ paths: description: "Get metadata about a Dataset\n> `Required Scope:` Namespace.Manage" summary: 'Get Dataset' tags: - - 'API Directory' + - 'API Directory (Administration)' security: - jwt: @@ -979,6 +1016,35 @@ paths: default: 0 format: double type: number + '/gateways/{gatewayId}/links': + get: + operationId: get-gateway-links + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + properties: {host: {type: string}} + required: [host] + type: object + type: array + description: "Get a summary of your endpoints\n> `Required Scope:` Namespace.Manage" + summary: 'Get endpoints' + tags: + - Gateways + security: + - + jwt: + - Namespace.Manage + parameters: + - + in: path + name: gatewayId + required: true + schema: + type: string '/gateways/{gatewayId}/directory/{id}': get: operationId: get-ns-directory-dataset @@ -990,7 +1056,7 @@ paths: schema: {} description: "Used primarily for \"Preview Mode\"\nGet a particular Dataset" tags: - - 'API Directory' + - 'API Directory (Administration)' security: - jwt: @@ -1019,7 +1085,7 @@ paths: schema: {} description: "Used primarily for \"Preview Mode\"\nList the datasets belonging to a particular namespace" tags: - - 'API Directory' + - 'API Directory (Administration)' security: - jwt: @@ -1464,7 +1530,7 @@ paths: $ref: '#/components/schemas/ActivityDetail' type: array description: '> `Required Scope:` Namespace.Assign' - summary: 'Get Namespace Activity for gateways associated with this Organization Unit' + summary: 'Get administration activity for gateways associated with this Organization Unit' tags: - Organizations security: @@ -1630,6 +1696,9 @@ tags: - name: 'API Directory' description: 'Discover all the great BC Government APIs' + - + name: 'API Directory (Administration)' + description: 'Administer datasets on the API Directory' - name: Organizations description: 'Manage organizational access control' diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index 2ca37bb37..2a90d4a04 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -13,7 +13,7 @@ import { EndpointsController } from './EndpointsController'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa import { NamespaceController } from './GatewayController'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa -import { NamespaceDirectoryController } from './GatewayDirectoryController'; +import { GatewayDirectoryController } from './GatewayDirectoryController'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa import { GatewayController } from './GatewayServicesController'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa @@ -38,6 +38,27 @@ const upload = multer(); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa const models: TsoaRoute.Models = { + "DatasetContact": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string"}, + "email": {"dataType":"string"}, + "role": {"dataType":"enum","enums":["pointOfContact"]}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DatasetResource": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string"}, + "name": {"dataType":"string"}, + "format": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["openapi-json"]},{"dataType":"enum","enums":["json"]}]}, + "url": {"dataType":"string"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "OrganizationRefID": { "dataType": "refAlias", "type": {"dataType":"string","validators":{}}, @@ -62,11 +83,11 @@ const models: TsoaRoute.Models = { "title": {"dataType":"string"}, "isInCatalog": {"dataType":"string"}, "isDraft": {"dataType":"string"}, - "contacts": {"dataType":"string"}, + "contacts": {"dataType":"array","array":{"dataType":"refObject","ref":"DatasetContact"}}, + "resources": {"dataType":"array","array":{"dataType":"refObject","ref":"DatasetResource"}}, "extSource": {"dataType":"string"}, "extRecordHash": {"dataType":"string"}, "tags": {"dataType":"array","array":{"dataType":"string"}}, - "resources": {"dataType":"any"}, "organization": {"ref":"OrganizationRefID"}, "organizationUnit": {"ref":"OrganizationUnitRefID"}, }, @@ -99,8 +120,8 @@ const models: TsoaRoute.Models = { "title": {"dataType":"string"}, "isInCatalog": {"dataType":"boolean"}, "isDraft": {"dataType":"boolean"}, - "contacts": {"dataType":"string"}, - "resources": {"dataType":"string"}, + "contacts": {"dataType":"array","array":{"dataType":"refObject","ref":"DatasetContact"}}, + "resources": {"dataType":"array","array":{"dataType":"refObject","ref":"DatasetResource"}}, "tags": {"dataType":"array","array":{"dataType":"string"}}, "organization": {"ref":"OrganizationRefID"}, "organizationUnit": {"ref":"OrganizationUnitRefID"}, @@ -829,10 +850,40 @@ export function RegisterRoutes(app: express.Router) { } }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get('/ds/api/v3/gateways/:gatewayId/links', + authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), + + async function NamespaceController_get(request: any, response: any, next: any) { + const args = { + gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(NamespaceController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.get.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.get('/ds/api/v3/gateways/:gatewayId/directory/:id', authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), - async function NamespaceDirectoryController_getDataset(request: any, response: any, next: any) { + async function GatewayDirectoryController_getDataset(request: any, response: any, next: any) { const args = { gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, id: {"in":"path","name":"id","required":true,"dataType":"string"}, @@ -847,7 +898,7 @@ export function RegisterRoutes(app: express.Router) { const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; - const controller: any = await container.get(NamespaceDirectoryController); + const controller: any = await container.get(GatewayDirectoryController); if (typeof controller['setStatus'] === 'function') { controller.setStatus(undefined); } @@ -863,7 +914,7 @@ export function RegisterRoutes(app: express.Router) { app.get('/ds/api/v3/gateways/:gatewayId/directory', authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), - async function NamespaceDirectoryController_getDatasets(request: any, response: any, next: any) { + async function GatewayDirectoryController_getDatasets(request: any, response: any, next: any) { const args = { gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, request: {"in":"request","name":"request","required":true,"dataType":"object"}, @@ -877,7 +928,7 @@ export function RegisterRoutes(app: express.Router) { const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; - const controller: any = await container.get(NamespaceDirectoryController); + const controller: any = await container.get(GatewayDirectoryController); if (typeof controller['setStatus'] === 'function') { controller.setStatus(undefined); } @@ -924,7 +975,7 @@ export function RegisterRoutes(app: express.Router) { app.get('/ds/api/v3/gateways/:gatewayId/services', authenticateMiddleware([{"jwt":["Namespace.Manage"]}]), - async function GatewayController_get(request: any, response: any, next: any) { + async function GatewayController_getServices(request: any, response: any, next: any) { const args = { gatewayId: {"in":"path","name":"gatewayId","required":true,"dataType":"string"}, request: {"in":"request","name":"request","required":true,"dataType":"object"}, @@ -944,7 +995,7 @@ export function RegisterRoutes(app: express.Router) { } - const promise = controller.get.apply(controller, validatedArgs as any); + const promise = controller.getServices.apply(controller, validatedArgs as any); promiseHandler(controller, promise, response, undefined, next); } catch (err) { return next(err); diff --git a/src/controllers/v3/t.js b/src/controllers/v3/t.js new file mode 100644 index 000000000..29b978e44 --- /dev/null +++ b/src/controllers/v3/t.js @@ -0,0 +1,33 @@ +function getMatchHostList(serviceName) { + return [ + `${serviceName}.api.gov.bc.ca`, + `${serviceName}-api-gov-bc-ca.dev.api.gov.bc.ca`, + `${serviceName}-api-gov-bc-ca.test.api.gov.bc.ca`, + ]; +} + +function isTaken(records, matchHosts) { + return ( + records.filter( + (r) => r.hosts.filter((h) => matchHosts.indexOf(h) >= 0).length > 0 + ).length > 0 + ); +} + +const records = [ + { + hosts: ['abc-api-gov-bc-ca.test.api.gov.bc.ca'], + }, +]; + +const serviceName = 'abc'; +let counter = 0; +let matchHostList; +do { + matchHostList = getMatchHostList( + counter == 0 ? serviceName : `${serviceName}-${counter}` + ); + counter++; +} while (isTaken(records, matchHostList)); + +console.log(matchHostList); diff --git a/src/controllers/v3/types.ts b/src/controllers/v3/types.ts index d59ce01d8..7157354c0 100644 --- a/src/controllers/v3/types.ts +++ b/src/controllers/v3/types.ts @@ -56,11 +56,11 @@ export interface Dataset { title?: string; isInCatalog?: string; isDraft?: string; - contacts?: string; + contacts?: DatasetContact[]; + resources?: DatasetResource[]; extSource?: string; extRecordHash?: string; tags?: string[]; - resources?: any; // toString organization?: OrganizationRefID; organizationUnit?: OrganizationUnitRefID; } @@ -96,8 +96,8 @@ export interface DraftDataset { title?: string; isInCatalog?: boolean; isDraft?: boolean; - contacts?: string; - resources?: string; + contacts?: DatasetContact[]; + resources?: DatasetResource[]; tags?: string[]; organization?: OrganizationRefID; organizationUnit?: OrganizationUnitRefID; @@ -513,6 +513,29 @@ export interface Blob { blob?: string; } + +/** + * @tsoaModel + * + */ +export interface DatasetContact { + name?: string; // Primary Key + email?: string; + role?: "pointOfContact"; +} + + +/** + * @tsoaModel + * + */ +export interface DatasetResource { + id?: string; // Primary Key + name?: string; + format?: "openapi-json" | "json"; + url?: string; +} + /** * @tsoaModel */ diff --git a/src/lists/extensions/Namespace.ts b/src/lists/extensions/Namespace.ts index c680dd23b..a8134540e 100644 --- a/src/lists/extensions/Namespace.ts +++ b/src/lists/extensions/Namespace.ts @@ -45,6 +45,7 @@ import { getAllNamespaces, getKeycloakGroupApi, getResource, + generateDisplayName, transformOrgAndOrgUnit, } from '../../services/keycloak/namespace-details'; import { newNamespaceID } from '../../services/identifiers'; @@ -478,7 +479,8 @@ module.exports = { ]; const res = { name: newNS, - displayName: args.displayName, + displayName: + args.displayName || generateDisplayName(context, newNS), type: 'namespace', resource_scopes: scopes, ownerManagedAccess: true, diff --git a/src/nextapp/.env.local b/src/nextapp/.env.local index 31bf55355..cd8e37950 100644 --- a/src/nextapp/.env.local +++ b/src/nextapp/.env.local @@ -4,7 +4,7 @@ NEXT_PUBLIC_KUBE_CLUSTER=local NEXT_PUBLIC_HELP_DESK_URL=https://dpdd.atlassian.net/servicedesk/customer/portal/1/group/2 NEXT_PUBLIC_HELP_CHAT_URL=https://chat.developer.gov.bc.ca/channel/aps-ops NEXT_PUBLIC_HELP_ISSUE_URL=https://github.com/bcgov/api-services-portal/issues -NEXT_PUBLIC_HELP_API_DOCS_URL=/ds/api/v2/console/ +NEXT_PUBLIC_HELP_API_DOCS_URL=/ds/api/v3/console/ NEXT_PUBLIC_HELP_SUPPORT_URL=https://developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/ NEXT_PUBLIC_HELP_RELEASE_URL=https://developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/reference/releases/ NEXT_PUBLIC_HELP_STATUS_URL=https://uptime.com/s/bcgov-dss diff --git a/src/services/keycloak/namespace-details.ts b/src/services/keycloak/namespace-details.ts index d7afeafd6..363bbfd95 100644 --- a/src/services/keycloak/namespace-details.ts +++ b/src/services/keycloak/namespace-details.ts @@ -174,3 +174,12 @@ export async function getResource( })) .pop(); } + +export function generateDisplayName(context: any, gatewayId: string): string { + logger.debug('[generateDisplayName] %j', context.req?.user); + if (context.req?.user?.provider_username) { + return `${context.req?.user?.provider_username}'s Gateway`; + } else { + return null; + } +} diff --git a/src/tsoa-v3.json b/src/tsoa-v3.json index 06d33b2f6..304cafe65 100644 --- a/src/tsoa-v3.json +++ b/src/tsoa-v3.json @@ -37,6 +37,10 @@ "name": "API Directory", "description": "Discover all the great BC Government APIs" }, + { + "name": "API Directory (Administration)", + "description": "Administer datasets on the API Directory" + }, { "name": "Organizations", "description": "Manage organizational access control" From 955b8dc241bf8d379e6c071556a963734bd1a43a Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Tue, 11 Jun 2024 22:50:41 -0700 Subject: [PATCH 053/191] remove unused file --- .../tests/19-api-v3/01-api-directory.ts | 6 ++++ src/controllers/v3/t.js | 33 ------------------- 2 files changed, 6 insertions(+), 33 deletions(-) delete mode 100644 src/controllers/v3/t.js diff --git a/e2e/cypress/tests/19-api-v3/01-api-directory.ts b/e2e/cypress/tests/19-api-v3/01-api-directory.ts index b036c60e9..6ad03ef39 100644 --- a/e2e/cypress/tests/19-api-v3/01-api-directory.ts +++ b/e2e/cypress/tests/19-api-v3/01-api-directory.ts @@ -89,6 +89,12 @@ describe('API Directory', () => { }, } expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + expect(JSON.stringify(body.contacts)).to.be.equal( + JSON.stringify(match.contacts) + ) + expect(JSON.stringify(body.resources)).to.be.equal( + JSON.stringify(match.resources) + ) }) } ) diff --git a/src/controllers/v3/t.js b/src/controllers/v3/t.js deleted file mode 100644 index 29b978e44..000000000 --- a/src/controllers/v3/t.js +++ /dev/null @@ -1,33 +0,0 @@ -function getMatchHostList(serviceName) { - return [ - `${serviceName}.api.gov.bc.ca`, - `${serviceName}-api-gov-bc-ca.dev.api.gov.bc.ca`, - `${serviceName}-api-gov-bc-ca.test.api.gov.bc.ca`, - ]; -} - -function isTaken(records, matchHosts) { - return ( - records.filter( - (r) => r.hosts.filter((h) => matchHosts.indexOf(h) >= 0).length > 0 - ).length > 0 - ); -} - -const records = [ - { - hosts: ['abc-api-gov-bc-ca.test.api.gov.bc.ca'], - }, -]; - -const serviceName = 'abc'; -let counter = 0; -let matchHostList; -do { - matchHostList = getMatchHostList( - counter == 0 ? serviceName : `${serviceName}-${counter}` - ); - counter++; -} while (isTaken(records, matchHostList)); - -console.log(matchHostList); From 3b5aaa62b230de9605fb608f02cf55c8d1f12ea3 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 13 Jun 2024 15:37:14 -0700 Subject: [PATCH 054/191] fix the org and product batch loading --- .../tests/19-api-v3/02-organization.ts | 8 +- src/batch/feed-worker.ts | 73 ++++++++++++------- .../connectExclusiveListCreate.ts | 4 +- src/test/integrated/batchworker/org.ts | 71 ++++++++++++++++++ src/test/integrated/batchworker/product.ts | 53 +++++++++----- 5 files changed, 161 insertions(+), 48 deletions(-) create mode 100644 src/test/integrated/batchworker/org.ts diff --git a/e2e/cypress/tests/19-api-v3/02-organization.ts b/e2e/cypress/tests/19-api-v3/02-organization.ts index 4f20d5f99..246ffde0d 100644 --- a/e2e/cypress/tests/19-api-v3/02-organization.ts +++ b/e2e/cypress/tests/19-api-v3/02-organization.ts @@ -35,10 +35,10 @@ describe('Organization', () => { { name: 'organization-admin', permissions: [ - // { - // resource: 'org/ministry-of-health', - // scopes: ['Dataset.Manage', 'GroupAccess.Manage', 'Namespace.Assign'], - // }, + { + resource: 'org/ministry-of-health', + scopes: ['Dataset.Manage', 'GroupAccess.Manage', 'Namespace.Assign'], + }, ], }, ], diff --git a/src/batch/feed-worker.ts b/src/batch/feed-worker.ts index f82c4eeaa..ec620d4ef 100644 --- a/src/batch/feed-worker.ts +++ b/src/batch/feed-worker.ts @@ -472,6 +472,13 @@ export const syncRecords = async function ( json.hasOwnProperty(md['refKey']) && json[md['refKey']] != localRecord[md['refKey']] ) { + logger.error( + '[syncRecords] (%s) %s != %s', + md['refKey'], + json[md['refKey']], + localRecord[md['refKey']] + ); + throw new Error('Unexpected ' + md['refKey']); } const transformKeys = @@ -613,7 +620,7 @@ export const applyTransformationsToNewCreation = async ( transformInfo: any, inputData: any, parentRecord: any -) => { +): Promise => { if (!inputData) { return; } @@ -627,36 +634,52 @@ export const applyTransformationsToNewCreation = async ( const transformKeys = 'transformations' in md ? Object.keys(md.transformations) : []; - for (const inputDataRecord of inputData) { - for (const transformKey of transformKeys) { - logger.debug( - ' -- (applyTransformations) changed trans? (%s)', - transformKey - ); - const transformInfo = md.transformations[transformKey]; + return Promise.all( + inputData.map(async (inputDataRecord: any) => { + const data: any = {}; + for (const field of md.sync) { + if (field in inputDataRecord) { + data[field] = inputDataRecord[field]; + } + } - if (transformInfo.filterByNamespace && parentRecord) { - inputDataRecord['_namespace'] = parentRecord['namespace']; + if (inputDataRecord.hasOwnProperty(md.refKey)) { + data[md.refKey] = inputDataRecord[md.refKey]; + } else if (inputDataRecord.hasOwnProperty('id')) { + data[md.refKey] = inputDataRecord['id']; } - const transformMutation = await transformations[transformInfo.name]( - keystone, - transformInfo, - null, - inputDataRecord, - transformKey - ); - delete inputDataRecord['_namespace']; - if (transformMutation && transformMutation != null) { + for (const transformKey of transformKeys) { logger.debug( - ' -- (applyTransformations) trans (%s) %j', - transformKey, - transformMutation + ' -- (applyTransformations) changed trans? (%s)', + transformKey ); - inputDataRecord[transformKey] = transformMutation; + const transformInfo = md.transformations[transformKey]; + + if (transformInfo.filterByNamespace && parentRecord) { + inputDataRecord['_namespace'] = parentRecord['namespace']; + } + + const transformMutation = await transformations[transformInfo.name]( + keystone, + transformInfo, + null, + inputDataRecord, + transformKey + ); + delete inputDataRecord['_namespace']; + if (transformMutation && transformMutation != null) { + logger.debug( + ' -- (applyTransformations) trans (%s) %j', + transformKey, + transformMutation + ); + data[transformKey] = transformMutation; + } } - } - } + return data; + }) + ); }; export const removeEmpty = (obj: object) => { diff --git a/src/batch/transformations/connectExclusiveListCreate.ts b/src/batch/transformations/connectExclusiveListCreate.ts index 88fa1949c..dec74db57 100644 --- a/src/batch/transformations/connectExclusiveListCreate.ts +++ b/src/batch/transformations/connectExclusiveListCreate.ts @@ -16,7 +16,7 @@ export async function connectExclusiveListCreate( ) { logger.debug('%s %j %j %j', fieldKey, currentData, inputData, parentRecord); - await applyTransformationsToNewCreation( + const createInputData = await applyTransformationsToNewCreation( keystone, transformInfo, inputData[fieldKey], @@ -35,7 +35,7 @@ export async function connectExclusiveListCreate( if (inputData[fieldKey]) { return { - create: inputData[fieldKey], + create: createInputData, }; } else { return null; diff --git a/src/test/integrated/batchworker/org.ts b/src/test/integrated/batchworker/org.ts new file mode 100644 index 000000000..10c11f841 --- /dev/null +++ b/src/test/integrated/batchworker/org.ts @@ -0,0 +1,71 @@ +/* +Wire up directly with Keycloak and use the Services +To run: +npm run ts-build +npm run ts-watch +node dist/test/integrated/batchworker/org.js +*/ + +import InitKeystone from '../keystonejs/init'; +import { + getRecords, + parseJsonString, + transformAllRefID, + removeEmpty, + removeKeys, + syncRecords, +} from '../../../batch/feed-worker'; +import { o } from '../util'; +import { BatchService } from '../../../services/keystone/batch-service'; + +(async () => { + const keystone = await InitKeystone(); + console.log('K = ' + keystone); + + const ns = 'platform'; + const skipAccessControl = true; + + const identity = { + id: null, + username: 'sample_username', + namespace: ns, + roles: JSON.stringify(['api-owner']), + scopes: [], + userId: null, + } as any; + + const ctx = keystone.createContext({ + skipAccessControl, + authentication: { item: identity }, + }); + + if (true) { + const json = { + id: '7a66db63-26f4-4052-9cd5-3272b63910f8', + type: 'organization', + name: 'ministry-of-health-7', + title: 'Ministry of Health', + extSource: '', + extRecordHash: '', + orgUnits: [ + { + id: '719b3297-846d-4b97-8095-ceb3ec505fb8', + name: 'planning-and-innovation-division-7', + title: 'Planning and Innovation', + extSource: '', + extRecordHash: '', + }, + { + id: '700b3297-846d-4b97-8095-ceb3ec505fb8', + name: 'health-dev-7', + title: 'Planning and Innovation', + extSource: '', + extRecordHash: '', + }, + ], + }; + const res = await syncRecords(ctx, 'Organization', json.id, json); + o(res); + } + await keystone.disconnect(); +})(); diff --git a/src/test/integrated/batchworker/product.ts b/src/test/integrated/batchworker/product.ts index eac8c069b..6cb8c444a 100644 --- a/src/test/integrated/batchworker/product.ts +++ b/src/test/integrated/batchworker/product.ts @@ -22,7 +22,7 @@ import { BatchService } from '../../../services/keystone/batch-service'; const keystone = await InitKeystone(); console.log('K = ' + keystone); - const ns = 'refactortime'; + const ns = 'platform'; const skipAccessControl = false; const identity = { @@ -39,21 +39,40 @@ import { BatchService } from '../../../services/keystone/batch-service'; authentication: { item: identity }, }); - const json = { - name: 'Refactor Time Test', - namespace: ns, - environments: [ - { - name: 'stage', - appId: '0A021EB0', - //services: [] as any, - services: ['a-service-for-refactortime'], - // services: ['a-service-for-refactortime', 'a-service-for-aps-moh-proto'], - }, - ] as any, - }; - const res = await syncRecords(ctx, 'Product', null, json); - o(res); - + if (false) { + const json = { + name: 'Refactor Time Test2', + namespace: ns, + environments: [ + { + name: 'stage', + appId: '0A021EB0', + //services: [] as any, + //services: ['a-service-for-refactortime'], + // services: ['a-service-for-refactortime', 'a-service-for-aps-moh-proto'], + }, + ] as any, + }; + const res = await syncRecords(ctx, 'Product', null, json); + o(res); + } + if (true) { + const json = { + name: 'Refactor Time Test 4', + appid: '348D98F1F56C', + namespace: ns, + environments: [ + { + name: 'stage', + appId: '3A021EB0', + legal: 'terms-of-use-for-api-gateway-1', + services: [] as any, + credentialIssuer: 'Gateway Services Resource Server', + }, + ], + }; + const res = await syncRecords(ctx, 'Product', null, json); + o(res); + } await keystone.disconnect(); })(); From b9d19d3ef7820ea90630765ee41cae957094b813 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Thu, 13 Jun 2024 15:47:41 -0700 Subject: [PATCH 055/191] fix unit test --- src/test/services/batch/testdata.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/test/services/batch/testdata.js b/src/test/services/batch/testdata.js index 93c4124ae..103cff5ec 100644 --- a/src/test/services/batch/testdata.js +++ b/src/test/services/batch/testdata.js @@ -31,18 +31,7 @@ export default { payload: { status: 200, result: 'created', - childResults: [ - { - status: 200, - result: 'created', - childResults: [], - }, - { - status: 200, - result: 'created', - childResults: [], - }, - ], + childResults: [], }, }, }, From d5e34e9334202ccc48e22a78c19cd682929dcea2 Mon Sep 17 00:00:00 2001 From: James Elson Date: Fri, 14 Jun 2024 14:03:00 -0700 Subject: [PATCH 056/191] My gateways page changes copied over --- .../namespace-manager/namespace-manager.tsx | 10 +- .../components/publishing-popover/index.ts | 1 + .../publishing-popover/publishing-popover.tsx | 120 ++++++ src/nextapp/pages/manager/gateways/index.tsx | 353 ++++++++++++++++++ 4 files changed, 479 insertions(+), 5 deletions(-) create mode 100644 src/nextapp/components/publishing-popover/index.ts create mode 100644 src/nextapp/components/publishing-popover/publishing-popover.tsx create mode 100644 src/nextapp/pages/manager/gateways/index.tsx diff --git a/src/nextapp/components/namespace-manager/namespace-manager.tsx b/src/nextapp/components/namespace-manager/namespace-manager.tsx index c846ebd93..a1ad8c5ed 100644 --- a/src/nextapp/components/namespace-manager/namespace-manager.tsx +++ b/src/nextapp/components/namespace-manager/namespace-manager.tsx @@ -90,10 +90,10 @@ const NamespaceManager: React.FC = ({ - Export Namespace Report + Export Gateway Report - Export a detailed report of your namespace metrics and + Export a detailed report of your gateway metrics and activities = ({ size="sm" data-testid="export-report-empty-text" > - You have no namespaces + You have no gateways - Create a namespace to manage. + Create a gateway to manage. )} @@ -181,7 +181,7 @@ const NamespaceManager: React.FC = ({ {isInvalid && ( - *Please select a namespace + *Please select a gateway )} diff --git a/src/nextapp/components/publishing-popover/index.ts b/src/nextapp/components/publishing-popover/index.ts new file mode 100644 index 000000000..be17cea1a --- /dev/null +++ b/src/nextapp/components/publishing-popover/index.ts @@ -0,0 +1 @@ +export { default } from './publishing-popover'; diff --git a/src/nextapp/components/publishing-popover/publishing-popover.tsx b/src/nextapp/components/publishing-popover/publishing-popover.tsx new file mode 100644 index 000000000..9cdcce5e9 --- /dev/null +++ b/src/nextapp/components/publishing-popover/publishing-popover.tsx @@ -0,0 +1,120 @@ +import * as React from 'react'; +import { + Icon, + Text, + Link, + Flex, + Center, + Popover, + PopoverTrigger, + PopoverContent, + PopoverBody, + PopoverArrow, +} from '@chakra-ui/react'; +import { FaClock, FaMinusCircle, FaCheckCircle } from 'react-icons/fa'; + +interface PublishingPopoverProps { + status: string; +} + +const PublishingPopover: React.FC = ({ status }) => { + return ( + <> + {status === 'disabled' && ( + + +
    + + + Publishing disabled + +
    +
    + + + + + + + Publishing disabled + + + + This means you still don't have permission to publish to the API + directory any API contained in this gateway. Request publishing + permission by{' '} + + adding an organization + {' '} + to your gateway. + + + +
    + )} + {status === 'pending' && ( + + +
    + + + Pending publishing permission + +
    +
    + + + + + + + Pending publishing permission + + + + This means you submitted a request to enable publishing + permission and is pending your Organization Administrator + approval. + + + +
    + )} + {status === 'enabled' && ( + + +
    + + + Publishing enabled + +
    +
    + + + + + + + Publishing enabled + + + + This means you are now allowed to publish to the API directory + any API contained in this gateway, so others can find and access + them. + + + +
    + )} + + ); +}; + +export default PublishingPopover; diff --git a/src/nextapp/pages/manager/gateways/index.tsx b/src/nextapp/pages/manager/gateways/index.tsx new file mode 100644 index 000000000..bee01eb8f --- /dev/null +++ b/src/nextapp/pages/manager/gateways/index.tsx @@ -0,0 +1,353 @@ +import * as React from 'react'; +import { + Box, + Container, + Icon, + Heading, + Text, + Link, + useDisclosure, + Button, + Flex, + Spacer, + useToast, + Select, + Center, +} from '@chakra-ui/react'; +import Head from 'next/head'; +import { gql } from 'graphql-request'; +import { FaPlus, FaLaptopCode, FaRocket, FaServer } from 'react-icons/fa'; +import { useQueryClient } from 'react-query'; +import { differenceInDays } from 'date-fns'; + +import PageHeader from '@/components/page-header'; +import GridLayout from '@/layouts/grid'; +import Card from '@/components/card'; +import { restApi, useApi } from '@/shared/services/api'; +import NamespaceManager from '@/components/namespace-manager/namespace-manager'; +import { Namespace } from '@/shared/types/query.types'; +import SearchInput from '@/components/search-input'; +import PublishingPopover from '@/components/publishing-popover'; + +type GatewayActions = { + title: string; + url: string; + urlText: string; + icon: React.ComponentType; + description: string; + descriptionEnd: string; +}; + +const actions: GatewayActions[] = [ + { + title: 'Need to create a new gateway?', + url: + 'https://developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/tutorials/quick-start/', + urlText: 'API Provider Quick Start', + icon: FaPlus, + description: 'Follow our', + descriptionEnd: 'guide.', + }, + { + title: 'GWA CLI commands', + url: + 'https://developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/resources/gwa-commands/', + urlText: 'GWA CLI', + icon: FaLaptopCode, + description: 'Explore helpful commands in our', + descriptionEnd: 'guide.', + }, + { + title: 'Ready to deploy to production?', + url: + 'https://developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/guides/owner-journey-v1/#production-links', + urlText: 'going to production', + icon: FaRocket, + description: 'Check our', + descriptionEnd: 'checklist.', + }, +]; + +const MyGatewaysPage: React.FC = () => { + const managerDisclosure = useDisclosure(); + const { data, isLoading, isSuccess, isError } = useApi( + 'allNamespaces', + { query }, + { suspense: false } + ); + const today = new Date(); + + // Namespace change + const client = useQueryClient(); + const toast = useToast(); + const handleNamespaceChange = React.useCallback( + (namespace: Namespace) => async () => { + toast({ + title: `Switching to ${namespace.name} namespace`, + status: 'info', + isClosable: true, + }); + try { + await restApi(`/admin/switch/${namespace.id}`, { method: 'PUT' }); + toast.closeAll(); + client.invalidateQueries(); + toast({ + title: `Switched to ${namespace.name} namespace`, + status: 'success', + isClosable: true, + }); + } catch (err) { + toast.closeAll(); + toast({ + title: 'Unable to switch namespaces', + status: 'error', + isClosable: true, + }); + } + }, + [client, toast] + ); + + // Filtering + const [filter, setFilter] = React.useState(''); + const handleFilterChange = React.useCallback( + (event: React.ChangeEvent) => { + setFilter(event.target.value); + }, + [] + ); + + // Search + const [search, setSearch] = React.useState(''); + const handleSearchChange = (value: string) => { + setSearch(value); + }; + + // Filter and search results + const filterBySearch = (result) => { + const regex = new RegExp( + search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'), + 'i' + ); + return result.filter( + (s) => regex.test(s.name) || regex.test(s.displayName) + ); + }; + const namespaceSearchResults = React.useMemo(() => { + const result = data?.allNamespaces ?? []; + if (search.trim()) { + if (filter === 'disabled') { + return filterBySearch(result).filter( + (s) => s.orgEnabled === false && !s.orgUpdatedAt + ); + } else if (filter === 'pending') { + return filterBySearch(result).filter( + (s) => s.orgEnabled === false && s.orgUpdatedAt + ); + } else if (filter === 'enabled') { + return filterBySearch(result).filter((s) => s.orgEnabled === true); + } else { + return filterBySearch(result); + } + } else { + if (filter === 'disabled') { + return result.filter((s) => s.orgEnabled === false && !s.orgUpdatedAt); + } else if (filter === 'pending') { + return result.filter((s) => s.orgEnabled === false && s.orgUpdatedAt); + } else if (filter === 'enabled') { + return result.filter((s) => s.orgEnabled === true); + } else { + return result; + } + } + }, [data, search, filter]); + + return ( + <> + + API Program Services | My Gateways + + + + Export Gateway Report + + } + title="My Gateways" + > + + {actions.map((action) => ( + + + + + + {action.title} + + + + {action.description}{' '} + + {action.urlText} + {' '} + {action.descriptionEnd} + + + + ))} + + + + + event.currentTarget.focus()} + onChange={handleSearchChange} + value={search} + data-testid="namespace-search-input" + /> + + {isSuccess && + (namespaceSearchResults.length === 1 ? ( + {namespaceSearchResults.length} gateway + ) : ( + {namespaceSearchResults.length} gateways + ))} + {isLoading && Loading gateways...} + {isError && Gateways failed to load} + {isSuccess && ( + <> + {namespaceSearchResults.map((namespace) => ( + + + + + + {namespace.displayName + ? namespace.displayName + : namespace.name} + + {differenceInDays( + today, + new Date(namespace.orgUpdatedAt) + ) <= 5 && ( +
    + + New + +
    + )} +
    + + {namespace.name} + +
    + + {namespace.orgEnabled === false && + !namespace.orgUpdatedAt && ( + + )} + {namespace.orgEnabled === false && namespace.orgUpdatedAt && ( + + )} + {namespace.orgEnabled === true && ( + + )} +
    + ))} + {namespaceSearchResults.length === 0 && ( + <> + + Empty folder + + + + No results found + + + + )} + + )} +
    +
    + {data && ( + + )} + + ); +}; + +export default MyGatewaysPage; + +const query = gql` + query GetNamespaces { + allNamespaces { + id + name + displayName + orgEnabled + orgUpdatedAt + } + } +`; From 3ebeada6f7aaf264b243c992ef49685d1b516055 Mon Sep 17 00:00:00 2001 From: James Elson Date: Tue, 18 Jun 2024 16:47:52 -0700 Subject: [PATCH 057/191] Added and updated breadcrumbs --- .../pages/manager/namespaces/index.tsx | 25 ++++++++++--------- src/nextapp/shared/hooks/index.ts | 1 + .../shared/hooks/use-namespace-breadcrumbs.ts | 3 ++- .../hooks/use-namespace-root-breadcrumbs.ts | 24 ++++++++++++++++++ 4 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 src/nextapp/shared/hooks/use-namespace-root-breadcrumbs.ts diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index cafc492e9..0d80b6697 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -23,7 +23,7 @@ import { Skeleton, Tooltip, VStack, - Stack + Stack, } from '@chakra-ui/react'; import ConfirmationDialog from '@/components/confirmation-dialog'; import Head from 'next/head'; @@ -53,13 +53,14 @@ import { RiApps2Fill } from 'react-icons/ri'; import PreviewBanner from '@/components/preview-banner'; import { QueryKey, useQueryClient } from 'react-query'; import { useRouter } from 'next/router'; -import Card from '@/components/card' -import GatewayGetStarted from '@/components/gateway-get-started' +import Card from '@/components/card'; +import GatewayGetStarted from '@/components/gateway-get-started'; import EmptyPane from '@/components/empty-pane'; import { Namespace, Query } from '@/shared/types/query.types'; import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; import { useGlobal } from '@/shared/services/global'; import EditNamespaceDisplayName from '@/components/edit-display-name'; +import { useNamespaceRootBreadcrumbs } from '@/shared/hooks'; const actions = [ { @@ -120,6 +121,7 @@ const secondaryActions = [ const NamespacesPage: React.FC = () => { const { user } = useAuth(); + const breadcrumbs = useNamespaceRootBreadcrumbs(); const hasNamespace = !!user?.namespace; const router = useRouter(); const toast = useToast(); @@ -271,15 +273,14 @@ const NamespacesPage: React.FC = () => { - - + + <> - {isError && ( - Gateways Failed to Load - )} - {isSuccess && data.allNamespaces.length == 0 && ( - - )} + {isError && Gateways Failed to Load} + {isSuccess && data.allNamespaces.length == 0 && } {hasNamespace && ( @@ -444,4 +445,4 @@ const query = gql` name } } -`; \ No newline at end of file +`; diff --git a/src/nextapp/shared/hooks/index.ts b/src/nextapp/shared/hooks/index.ts index ac3f95bc2..48f490298 100644 --- a/src/nextapp/shared/hooks/index.ts +++ b/src/nextapp/shared/hooks/index.ts @@ -1 +1,2 @@ export { default as useNamespaceBreadcrumbs } from './use-namespace-breadcrumbs'; +export { default as useNamespaceRootBreadcrumbs } from './use-namespace-root-breadcrumbs'; diff --git a/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts b/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts index 42edff41d..1630428ee 100644 --- a/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts +++ b/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts @@ -12,7 +12,8 @@ const useNamespaceBreadcrumbs = ( if (user) { return [ - { href: '/manager/namespaces', text: `Namespaces (${user.namespace})` }, + { href: '/manager/gateways', text: 'My Gateways' }, + { href: '/manager/namespaces', text: `Gateway (${user.namespace})` }, ...appendedBreadcrumbs, ]; } diff --git a/src/nextapp/shared/hooks/use-namespace-root-breadcrumbs.ts b/src/nextapp/shared/hooks/use-namespace-root-breadcrumbs.ts new file mode 100644 index 000000000..28ad4558e --- /dev/null +++ b/src/nextapp/shared/hooks/use-namespace-root-breadcrumbs.ts @@ -0,0 +1,24 @@ +import { useAuth } from '@/shared/services/auth'; + +type Breadcrumb = { + href?: string; + text: string; +}; + +const useNamespaceRootBreadcrumbs = (): Breadcrumb[] => { + const { user } = useAuth(); + + if (user && user.namespace) { + return [ + { href: '/manager/gateways', text: 'My Gateways' }, + { text: `Gateway (${user.namespace})` }, + ]; + } + + return [ + { href: '/manager/gateways', text: 'My Gateways' }, + { text: 'Gateway' }, + ]; +}; + +export default useNamespaceRootBreadcrumbs; From 79fef164b42e1b6771f4d0abf2bfb533204f5ea8 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Wed, 19 Jun 2024 11:39:24 -0700 Subject: [PATCH 058/191] redirect provider to /manager/gateways on login --- src/nextapp/pages/login.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextapp/pages/login.tsx b/src/nextapp/pages/login.tsx index bbef8ca3c..3d7f726be 100644 --- a/src/nextapp/pages/login.tsx +++ b/src/nextapp/pages/login.tsx @@ -46,7 +46,7 @@ const LoginPage: React.FC< React.useEffect(() => { if (ok) { - router.push('/devportal/api-directory'); + router.push(isProvider ? '/manager/gateways' : '/devportal/api-directory'); } }, [ok]); From 4e9a2142790137c03d63efdba8de10187143ecb7 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 19 Jun 2024 11:49:03 -0700 Subject: [PATCH 059/191] upd namespace display name validation and default value --- README.md | 20 +++-- e2e/cypress/tests/19-api-v3/03-gateways.ts | 83 ++++++++++++++++++- local/oauth2-proxy/oauth2-proxy-dev.cfg | 2 +- src/lists/extensions/Namespace.ts | 23 ++--- src/services/keycloak/namespace-details.ts | 28 ++++++- .../uma2/resource-registration-service.ts | 9 -- 6 files changed, 133 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index f0748a572..ddef88476 100644 --- a/README.md +++ b/README.md @@ -67,17 +67,19 @@ Use the following configuration to run the Portal locally (outside of Docker) ag 1. If using Node version > 17, run `npm install --legacy-peer-deps` -1. Turn off the docker compose Portal: `docker stop apsportal` -1. Configure the `oauth2-proxy` that is running in Docker: +1. Turn off the docker compose Portal and OAuth2 Proxy: `docker stop apsportal oauth2-proxy` - 1. Update `upstreams` in `local/oauth2-proxy/oauth2-proxy-local.cfg` to include the IP address of your local machine, e.g. `upstreams=["http://172.100.100.01:3000"]` -
    You can obtain the IP address using `hostname -I`. +1. Start the OAuth2 Proxy locally: - 1. Restart the oauth2-proxy: `docker compose restart oauth2-proxy` - 1. Update `DESTINATION_URL` in `local/feeds/.env.local` to include the IP address of your local machine - 1. Restart the feeder: `docker compose restart feeder` - 1. Update `PORTAL_ACTIVITY_URL` in `local/gwa-api/.env.local` to include the IP address of your local machine - 1. Restart the feeder: `docker compose restart gwa-api` +```sh +hostip=$(ifconfig en0 | awk '$1 == "inet" {print $2}') + +docker run -ti --rm --name proxy --net=host \ + --add-host portal.localtest.me:$hostip \ + -v `pwd`/local/oauth2-proxy/oauth2-proxy-dev.cfg:/oauth2.config \ + quay.io/oauth2-proxy/oauth2-proxy:v7.2.0 \ + --config /oauth2.config +``` 1. Start the Portal locally: diff --git a/e2e/cypress/tests/19-api-v3/03-gateways.ts b/e2e/cypress/tests/19-api-v3/03-gateways.ts index 0b377ce0c..d31c77a98 100644 --- a/e2e/cypress/tests/19-api-v3/03-gateways.ts +++ b/e2e/cypress/tests/19-api-v3/03-gateways.ts @@ -42,13 +42,50 @@ describe('Gateways', () => { ) }) + it('POST /gateways (with no payload)', () => { + const { v4: uuidv4 } = require('uuid') + const payload = {} + cy.log(JSON.stringify(payload)) + cy.setRequestBody(payload) + cy.callAPI('ds/api/v3/gateways', 'POST').then( + ({ apiRes: { body, status } }: any) => { + const match = { + gatewayId: body.gatewayId, + displayName: "janis's Gateway", + } + expect(status).to.be.equal(200) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + it('POST /gateways (with gatewayId)', () => { const { v4: uuidv4 } = require('uuid') const customId = uuidv4().replace(/-/g, '').toLowerCase().substring(0, 3) const payload = { gatewayId: `custom-${customId}-gw`, - displayName: 'My ABC Gateway', + displayName: 'ABC GW', + } + cy.log(JSON.stringify(payload)) + cy.setRequestBody(payload) + cy.callAPI('ds/api/v3/gateways', 'POST').then( + ({ apiRes: { body, status } }: any) => { + cy.log(body) + expect(status).to.be.equal(200) + expect(body.gatewayId).to.be.equal(payload.gatewayId) + expect(body.displayName).to.be.equal(payload.displayName) + } + ) + }) + + it('POST /gateways (with all valid chars)', () => { + const { v4: uuidv4 } = require('uuid') + const customId = uuidv4().replace(/-/g, '').toLowerCase().substring(0, 3) + + const payload = { + gatewayId: `custom-${customId}-gw`, + displayName: 'ABC GW with ( ) - _ / . chars', } cy.log(JSON.stringify(payload)) cy.setRequestBody(payload) @@ -140,5 +177,49 @@ describe('Gateways', () => { } ) }) + it('POST /gateways (display name too long)', () => { + const payload = { + displayName: 'this display name is more than 30 characters', + } + cy.log(JSON.stringify(payload)) + cy.setRequestBody(payload) + cy.callAPI('ds/api/v3/gateways', 'POST').then( + ({ apiRes: { body, status } }: any) => { + const match = { + message: 'Validation Failed', + details: { + d0: { + message: + 'Display name can not be longer than 30 characters and can only use special characters "-()_ .\'/".', + }, + }, + } + expect(status).to.be.equal(422) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + it('POST /gateways (display name invalid characters)', () => { + const payload = { + displayName: 'this display name has invalid # char', + } + cy.log(JSON.stringify(payload)) + cy.setRequestBody(payload) + cy.callAPI('ds/api/v3/gateways', 'POST').then( + ({ apiRes: { body, status } }: any) => { + const match = { + message: 'Validation Failed', + details: { + d0: { + message: + 'Display name can not be longer than 30 characters and can only use special characters "-()_ .\'/".', + }, + }, + } + expect(status).to.be.equal(422) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) }) }) diff --git a/local/oauth2-proxy/oauth2-proxy-dev.cfg b/local/oauth2-proxy/oauth2-proxy-dev.cfg index d616f19c1..80b0fa158 100644 --- a/local/oauth2-proxy/oauth2-proxy-dev.cfg +++ b/local/oauth2-proxy/oauth2-proxy-dev.cfg @@ -23,5 +23,5 @@ set_authorization_header="false" pass_authorization_header="false" skip_auth_regex="/__coverage__|/login|/health|/public|/docs|/redirect|/_next|/images|/devportal|/manager|/about|/maintenance|/admin/session|/ds/api|/gw/api|/feed/|/signout|^[/]$" whitelist_domains="keycloak.localtest.me:9081" -upstreams=["http://192.168.1.69:3000"] +upstreams=["http://portal.localtest.me:3000"] skip_provider_button='true' diff --git a/src/lists/extensions/Namespace.ts b/src/lists/extensions/Namespace.ts index cf0e56fc5..918b93b15 100644 --- a/src/lists/extensions/Namespace.ts +++ b/src/lists/extensions/Namespace.ts @@ -47,6 +47,8 @@ import { getResource, generateDisplayName, transformOrgAndOrgUnit, + validateDisplayName, + validateNamespaceName, } from '../../services/keycloak/namespace-details'; import { newNamespaceID } from '../../services/identifiers'; @@ -145,7 +147,8 @@ module.exports = { const resource: any = await getResource(selectedNS, envCtx); merged['id'] = resource['id']; merged['scopes'] = resource['scopes']; - merged['displayName'] = resource['displayName']; + merged['displayName'] = + resource['displayName'] || `Gateway ${resource['name']}`; } if (merged.org) { @@ -362,6 +365,8 @@ module.exports = { const ns = context.req.user?.namespace; + validateDisplayName(displayName); + const prodEnv = await getGwaProductEnvironment(context, true); await getNamespaceResourceSets(prodEnv); // sets accessToken @@ -463,15 +468,14 @@ module.exports = { info: any, { query, access }: any ) => { - const namespaceValidationRule = '^[a-z][a-z0-9-]{3,13}[a-z0-9]$'; - const newNS = args.name ? args.name : newNamespaceID(); - regExprValidation( - namespaceValidationRule, - newNS, - 'Namespace name must be between 5 and 15 alpha-numeric lowercase characters and start and end with an alphabet.' - ); + validateNamespaceName(newNS); + + const displayName = + args.displayName || generateDisplayName(context, newNS); + + validateDisplayName(displayName); const noauthContext = context.createContext({ skipAccessControl: true, @@ -514,8 +518,7 @@ module.exports = { ]; const res = { name: newNS, - displayName: - args.displayName || generateDisplayName(context, newNS), + displayName, type: 'namespace', resource_scopes: scopes, ownerManagedAccess: true, diff --git a/src/services/keycloak/namespace-details.ts b/src/services/keycloak/namespace-details.ts index cb2f26421..058f4e434 100644 --- a/src/services/keycloak/namespace-details.ts +++ b/src/services/keycloak/namespace-details.ts @@ -1,4 +1,8 @@ -import { camelCaseAttributes, transformSingleValueAttributes } from '../utils'; +import { + camelCaseAttributes, + regExprValidation, + transformSingleValueAttributes, +} from '../utils'; import { IssuerEnvironmentConfig } from '../workflow/types'; import { KeycloakGroupService } from './group-service'; @@ -170,7 +174,7 @@ export async function getResource( .map((ns: ResourceSet) => ({ id: ns.id, name: ns.name, - displayName: ns.displayName, + displayName: ns.displayName || `Gateway ${ns.name}`, scopes: ns.resource_scopes, })) .pop(); @@ -184,3 +188,23 @@ export function generateDisplayName(context: any, gatewayId: string): string { return null; } } + +export function validateNamespaceName(name: string) { + const namespaceValidationRule = '^[a-z][a-z0-9-]{3,13}[a-z0-9]$'; + + regExprValidation( + namespaceValidationRule, + name, + 'Namespace name must be between 5 and 15 alpha-numeric lowercase characters and start and end with an alphabet.' + ); +} + +export function validateDisplayName(displayName: string) { + const displayNameValidationRule = "^[A-Za-z0-9-()_ .'\\/]{0,30}$"; + + regExprValidation( + displayNameValidationRule, + displayName, + 'Display name can not be longer than 30 characters and can only use special characters "-()_ .\'/".' + ); +} diff --git a/src/services/uma2/resource-registration-service.ts b/src/services/uma2/resource-registration-service.ts index a16b5e6a4..65a8e65d6 100644 --- a/src/services/uma2/resource-registration-service.ts +++ b/src/services/uma2/resource-registration-service.ts @@ -3,7 +3,6 @@ import fetch from 'node-fetch'; import { Logger } from '../../logger'; import querystring from 'querystring'; import { headers } from '../keycloak/keycloak-api'; -import { regExprValidation } from '../utils'; const logger = Logger('uma2-resource'); @@ -98,14 +97,6 @@ export class UMAResourceRegistrationService { } public async updateDisplayName(name: string, displayName: string) { - const displayNameValidationRule = '^[A-Za-z0-9-()_ ]{0,50}$'; - - regExprValidation( - displayNameValidationRule, - displayName, - 'Display name can not be longer than 50 characters and can only use special characters "-()_ ".' - ); - const before = await this.findResourceByName(name); await this.updateResourceSet({ From 8eb965a08246976589e99e7f8a8654a75dd5a3df Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 19 Jun 2024 12:18:56 -0700 Subject: [PATCH 060/191] upd route endpoint availability --- e2e/cypress/tests/19-api-v3/07-endpoints.ts | 25 +++++++++++ src/controllers/v3/EndpointsController.ts | 47 +++++++++++++-------- src/controllers/v3/openapi.yaml | 6 +++ src/controllers/v3/routes.ts | 1 + 4 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 e2e/cypress/tests/19-api-v3/07-endpoints.ts diff --git a/e2e/cypress/tests/19-api-v3/07-endpoints.ts b/e2e/cypress/tests/19-api-v3/07-endpoints.ts new file mode 100644 index 000000000..1ca6a1a90 --- /dev/null +++ b/e2e/cypress/tests/19-api-v3/07-endpoints.ts @@ -0,0 +1,25 @@ +describe('Endpoints', () => { + it('GET /routes/availability', () => { + cy.callAPI( + 'ds/api/v3/routes/availability?gatewayId=gw-1234&serviceName=testme', + 'GET' + ).then(({ apiRes: { status, body } }: any) => { + const match = { + available: true, + suggestion: { + serviceName: 'testme', + names: ['testme', 'testme-dev', 'testme-test'], + hosts: [ + 'testme.api.gov.bc.ca', + 'testme.api.dev.gov.bc.ca', + 'testme.api.test.gov.bc.ca', + 'testme-api-gov-bc-ca.dev.api.gov.bc.ca', + 'testme-api-gov-bc-ca.test.api.gov.bc.ca', + ], + }, + } + expect(status).to.be.equal(200) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) + }) +}) diff --git a/src/controllers/v3/EndpointsController.ts b/src/controllers/v3/EndpointsController.ts index fa44fc6e9..2f386928f 100644 --- a/src/controllers/v3/EndpointsController.ts +++ b/src/controllers/v3/EndpointsController.ts @@ -12,7 +12,13 @@ import { import { KeystoneService } from '../ioc/keystoneInjector'; import { inject, injectable } from 'tsyringe'; import { getRecords, removeEmpty } from '../../batch/feed-worker'; -import { GatewayRoute } from './types'; +import { GatewayRoute, GatewayService } from './types'; + +interface MatchList { + serviceName: string; + names: string[]; + hosts: string[]; +} @injectable() @Route('/routes') @@ -28,50 +34,57 @@ export class EndpointsController extends Controller { @OperationId('check-availability') public async check( @Query() serviceName: string, + @Query() gatewayId: string, @Request() request: any ): Promise { const ctx = this.keystone.sudo(); - const records = await getRecords( - ctx, - 'GatewayRoute', - 'allGatewayRoutes', - [] - ); + const records = await getRecords(ctx, 'GatewayRoute', 'allGatewayRoutes', [ + 'service', + ]); let counter = 0; - let matchHostList; + let matchHostList: MatchList; do { counter++; matchHostList = this.getMatchHostList( - counter == 1 ? serviceName : `${serviceName}-${counter}` + counter == 1 + ? serviceName + : counter == 2 + ? `${gatewayId}-${serviceName}` + : `${gatewayId}-${counter}-${serviceName}` ); - } while (this.isTaken(records, matchHostList.hosts)); + } while (this.isTaken(records, matchHostList)); return { available: counter == 1, - name: matchHostList.serviceName, - host: matchHostList.hosts[0], + suggestion: matchHostList, }; } - private getMatchHostList( - serviceName: string - ): { serviceName: string; hosts: string[] } { + private getMatchHostList(serviceName: string): MatchList { return { serviceName, + names: [`${serviceName}`, `${serviceName}-dev`, `${serviceName}-test`], hosts: [ `${serviceName}.api.gov.bc.ca`, + `${serviceName}.api.dev.gov.bc.ca`, + `${serviceName}.api.test.gov.bc.ca`, `${serviceName}-api-gov-bc-ca.dev.api.gov.bc.ca`, `${serviceName}-api-gov-bc-ca.test.api.gov.bc.ca`, ], }; } - private isTaken(records: any[], matchHosts: string[]): boolean { + private isTaken(records: any[], matchList: MatchList): boolean { return ( records.filter( (r: GatewayRoute) => - r.hosts.filter((h: string) => matchHosts.indexOf(h) >= 0).length > 0 + r.hosts.filter((h: string) => matchList.hosts.indexOf(h) >= 0) + .length > 0 || + matchList.names.filter((n: string) => n == r.name).length > 0 || + matchList.names.filter( + (n: string) => n == (r.service as GatewayService).name + ).length > 0 ).length > 0 ); } diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 64a12a4ec..cd84dd256 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -853,6 +853,12 @@ paths: required: true schema: type: string + - + in: query + name: gatewayId + required: true + schema: + type: string /gateways/report: get: operationId: report diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index 2a90d4a04..3de19b1d6 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -645,6 +645,7 @@ export function RegisterRoutes(app: express.Router) { async function EndpointsController_check(request: any, response: any, next: any) { const args = { serviceName: {"in":"query","name":"serviceName","required":true,"dataType":"string"}, + gatewayId: {"in":"query","name":"gatewayId","required":true,"dataType":"string"}, request: {"in":"request","name":"request","required":true,"dataType":"object"}, }; From 72f363d721df64afe96248fa7df284ab625d8a8d Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Wed, 19 Jun 2024 12:21:25 -0700 Subject: [PATCH 061/191] restrict new My Gateways to signed in portal-user --- .../manager/{namespaces => gateways}/get-started.tsx | 0 src/nextapp/shared/data/links.ts | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/nextapp/pages/manager/{namespaces => gateways}/get-started.tsx (100%) diff --git a/src/nextapp/pages/manager/namespaces/get-started.tsx b/src/nextapp/pages/manager/gateways/get-started.tsx similarity index 100% rename from src/nextapp/pages/manager/namespaces/get-started.tsx rename to src/nextapp/pages/manager/gateways/get-started.tsx diff --git a/src/nextapp/shared/data/links.ts b/src/nextapp/shared/data/links.ts index 3180ae586..2165b5163 100644 --- a/src/nextapp/shared/data/links.ts +++ b/src/nextapp/shared/data/links.ts @@ -48,19 +48,19 @@ const links: NavLink[] = [ sites: ['platform'], }, { - name: 'Namespaces', - url: '/manager/namespaces', + name: 'Gateways', + url: '/manager/gateways', access: ['portal-user'], sites: ['devportal'], }, { name: 'Gateways Get Started', - url: '/manager/namespaces/get-started', + url: '/manager/gateways/get-started', access: ['portal-user'], sites: ['devportal'], }, { - name: 'Namespaces', + name: 'Gateways', altUrls: [ '/manager/services', '/manager/services/[id]', From 7c82ace9cb523bde7bf4e299e927f94941c21824 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Wed, 19 Jun 2024 12:30:55 -0700 Subject: [PATCH 062/191] hide gateway selector on Get Started page --- src/nextapp/components/nav-bar/nav-bar.tsx | 2 +- src/nextapp/pages/_app.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nextapp/components/nav-bar/nav-bar.tsx b/src/nextapp/components/nav-bar/nav-bar.tsx index 0c39fad54..65a5ddac2 100644 --- a/src/nextapp/components/nav-bar/nav-bar.tsx +++ b/src/nextapp/components/nav-bar/nav-bar.tsx @@ -100,7 +100,7 @@ const NavBar: React.FC = ({ site, links, pathname }) => { ))}
    - {(pathname.startsWith('/manager/') || pathname === '/devportal/api-directory/your-products') && ( + {((pathname.startsWith('/manager/') && pathname !== '/manager/gateways/get-started') || pathname === '/devportal/api-directory/your-products') && ( = ({ Component, pageProps }) => { }, [router]); // Temp solution for handing spacing around new gateways dropdown menu - const gatewaysMenu = (router?.pathname.startsWith('/manager/') || router?.pathname === '/devportal/api-directory/your-products') + const gatewaysMenu = ((router?.pathname.startsWith('/manager/') && router?.pathname !== '/manager/gateways/get-started') || router?.pathname === '/devportal/api-directory/your-products') if (!queryClientRef.current) { queryClientRef.current = new QueryClient({ From 2635f0f8e4663abf68c86bd74d40f41770991c84 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Wed, 19 Jun 2024 12:41:25 -0700 Subject: [PATCH 063/191] use consts for help URLs --- src/nextapp/.env.local | 4 ++-- .../gateway-get-started.tsx | 20 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/nextapp/.env.local b/src/nextapp/.env.local index cd8e37950..1cb349776 100644 --- a/src/nextapp/.env.local +++ b/src/nextapp/.env.local @@ -5,7 +5,7 @@ NEXT_PUBLIC_HELP_DESK_URL=https://dpdd.atlassian.net/servicedesk/customer/portal NEXT_PUBLIC_HELP_CHAT_URL=https://chat.developer.gov.bc.ca/channel/aps-ops NEXT_PUBLIC_HELP_ISSUE_URL=https://github.com/bcgov/api-services-portal/issues NEXT_PUBLIC_HELP_API_DOCS_URL=/ds/api/v3/console/ -NEXT_PUBLIC_HELP_SUPPORT_URL=https://developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/ -NEXT_PUBLIC_HELP_RELEASE_URL=https://developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/reference/releases/ +NEXT_PUBLIC_HELP_SUPPORT_URL=https://dev.developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/ +NEXT_PUBLIC_HELP_RELEASE_URL=https://dev.developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/reference/releases/ NEXT_PUBLIC_HELP_STATUS_URL=https://uptime.com/s/bcgov-dss diff --git a/src/nextapp/components/gateway-get-started/gateway-get-started.tsx b/src/nextapp/components/gateway-get-started/gateway-get-started.tsx index 85f448031..bc39add1e 100644 --- a/src/nextapp/components/gateway-get-started/gateway-get-started.tsx +++ b/src/nextapp/components/gateway-get-started/gateway-get-started.tsx @@ -46,6 +46,10 @@ function SmoothScrollLink({ href, children }) { const GatewayGetStarted: React.FC = () => { const global = useGlobal(); + const GlossaryUrl = global?.helpLinks.helpSupportUrl + 'reference/glossary' + const QuickStartUrl = global?.helpLinks.helpSupportUrl + 'tutorials/quick-start' + const GwaInstallUrl = global?.helpLinks.helpSupportUrl + 'how-to/gwa-install' + const GwaCommandsUrl = global?.helpLinks.helpSupportUrl + 'resources/gwa-commands' return ( <> @@ -109,7 +113,7 @@ const GatewayGetStarted: React.FC = () => { Follow these steps to create and configure your first gateway. For more details on how to set up an API, consult our API provider{' '} { Our CLI is a convenient way to configure your gateways.{' '} { { These useful commands help you manage your gateway resources. For more details visit our {' '} { { Date: Wed, 19 Jun 2024 15:48:33 -0700 Subject: [PATCH 064/191] Breadcrumbs display name support --- .../shared/hooks/use-namespace-breadcrumbs.ts | 30 ++++++++++++++----- .../hooks/use-namespace-root-breadcrumbs.ts | 26 +++++++++------- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts b/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts index 1630428ee..db6e0a9ff 100644 --- a/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts +++ b/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts @@ -1,4 +1,4 @@ -import { useAuth } from '@/shared/services/auth'; +import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; type Breadcrumb = { href?: string; @@ -8,14 +8,28 @@ type Breadcrumb = { const useNamespaceBreadcrumbs = ( appendedBreadcrumbs: Breadcrumb[] = [] ): Breadcrumb[] => { - const { user } = useAuth(); + const namespace = useCurrentNamespace(); - if (user) { - return [ - { href: '/manager/gateways', text: 'My Gateways' }, - { href: '/manager/namespaces', text: `Gateway (${user.namespace})` }, - ...appendedBreadcrumbs, - ]; + if (namespace.isSuccess && !namespace.isFetching) { + if (namespace.data?.currentNamespace?.displayName) { + return [ + { href: '/manager/gateways', text: 'My Gateways' }, + { + href: '/manager/namespaces', + text: `${namespace.data?.currentNamespace?.displayName}`, + }, + ...appendedBreadcrumbs, + ]; + } else { + return [ + { href: '/manager/gateways', text: 'My Gateways' }, + { + href: '/manager/namespaces', + text: `${namespace.data?.currentNamespace?.name}`, + }, + ...appendedBreadcrumbs, + ]; + } } return appendedBreadcrumbs; diff --git a/src/nextapp/shared/hooks/use-namespace-root-breadcrumbs.ts b/src/nextapp/shared/hooks/use-namespace-root-breadcrumbs.ts index 28ad4558e..b094ecffb 100644 --- a/src/nextapp/shared/hooks/use-namespace-root-breadcrumbs.ts +++ b/src/nextapp/shared/hooks/use-namespace-root-breadcrumbs.ts @@ -1,4 +1,4 @@ -import { useAuth } from '@/shared/services/auth'; +import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; type Breadcrumb = { href?: string; @@ -6,19 +6,23 @@ type Breadcrumb = { }; const useNamespaceRootBreadcrumbs = (): Breadcrumb[] => { - const { user } = useAuth(); + const namespace = useCurrentNamespace(); - if (user && user.namespace) { - return [ - { href: '/manager/gateways', text: 'My Gateways' }, - { text: `Gateway (${user.namespace})` }, - ]; + if (namespace.isSuccess && !namespace.isFetching) { + if (namespace.data?.currentNamespace?.displayName) { + return [ + { href: '/manager/gateways', text: 'My Gateways' }, + { text: `${namespace.data?.currentNamespace?.displayName}` }, + ]; + } else { + return [ + { href: '/manager/gateways', text: 'My Gateways' }, + { text: `${namespace.data?.currentNamespace?.name}` }, + ]; + } } - return [ - { href: '/manager/gateways', text: 'My Gateways' }, - { text: 'Gateway' }, - ]; + return [{ href: '/manager/gateways', text: 'My Gateways' }]; }; export default useNamespaceRootBreadcrumbs; From 58ae1742095f1a27a0c10263b65211899e55a34e Mon Sep 17 00:00:00 2001 From: ike thecoder Date: Thu, 20 Jun 2024 11:07:45 -0700 Subject: [PATCH 065/191] upd proxy to gwa-api to keep using v2 --- src/api-proxy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api-proxy.js b/src/api-proxy.js index a12b26210..a7e149c54 100644 --- a/src/api-proxy.js +++ b/src/api-proxy.js @@ -17,7 +17,7 @@ class ApiProxyApp { logLevel: 'debug', pathRewrite: { '^/gw/api/v2/': '/v2/', - '^/gw/api/v3/': '/v3/', + '^/gw/api/v3/': '/v2/', }, onProxyReq: (proxyReq, req) => { //console.log(req.headers) From 03ba6b21225d564d1e887ecd642246988e713972 Mon Sep 17 00:00:00 2001 From: ike thecoder Date: Thu, 20 Jun 2024 11:51:45 -0700 Subject: [PATCH 066/191] Update api-proxy to gwa-api to fix routing to v2 --- src/api-proxy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api-proxy.js b/src/api-proxy.js index a7e149c54..8e3c57376 100644 --- a/src/api-proxy.js +++ b/src/api-proxy.js @@ -17,7 +17,7 @@ class ApiProxyApp { logLevel: 'debug', pathRewrite: { '^/gw/api/v2/': '/v2/', - '^/gw/api/v3/': '/v2/', + '^/gw/api/v3/gateways/': '/v2/namespaces/' }, onProxyReq: (proxyReq, req) => { //console.log(req.headers) From 42420fdfc5b0889be045977ba447721d222e60e8 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Thu, 20 Jun 2024 12:05:19 -0700 Subject: [PATCH 067/191] use existing whitelisted query --- src/nextapp/pages/manager/gateways/get-started.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nextapp/pages/manager/gateways/get-started.tsx b/src/nextapp/pages/manager/gateways/get-started.tsx index 1e8f936b2..48a333baf 100644 --- a/src/nextapp/pages/manager/gateways/get-started.tsx +++ b/src/nextapp/pages/manager/gateways/get-started.tsx @@ -45,6 +45,7 @@ export default NamespacesPage; const query = gql` query GetNamespaces { allNamespaces { + id name } } From 50174cee61dd6eee4fee1b63b9d1e8dc42745859 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Thu, 20 Jun 2024 12:18:19 -0700 Subject: [PATCH 068/191] conditionally push to Get Started page, simplify query --- .../graphql-whitelist/httplocalhost4180-afc05a.gql | 1 - src/nextapp/pages/manager/gateways/get-started.tsx | 10 ++++------ src/nextapp/pages/manager/gateways/index.tsx | 9 +++++++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/authz/graphql-whitelist/httplocalhost4180-afc05a.gql b/src/authz/graphql-whitelist/httplocalhost4180-afc05a.gql index ffb1ee232..36abce8e3 100644 --- a/src/authz/graphql-whitelist/httplocalhost4180-afc05a.gql +++ b/src/authz/graphql-whitelist/httplocalhost4180-afc05a.gql @@ -1,7 +1,6 @@ query GetNamespaces { allNamespaces { - id name } } diff --git a/src/nextapp/pages/manager/gateways/get-started.tsx b/src/nextapp/pages/manager/gateways/get-started.tsx index 48a333baf..aba2b9377 100644 --- a/src/nextapp/pages/manager/gateways/get-started.tsx +++ b/src/nextapp/pages/manager/gateways/get-started.tsx @@ -29,11 +29,10 @@ const NamespacesPage: React.FC = () => { {isError && ( Gateways Failed to Load )} - {/* TODO: add data.allNamespaces.length == 0 to the router logic in order to show this page */} - {/* {isSuccess && data.allNamespaces.length == 0 && ( */} - {isSuccess && ( - - )} + {(isSuccess && data.allNamespaces.length != 0) && ( + Gateways Found! + )} +
    @@ -45,7 +44,6 @@ export default NamespacesPage; const query = gql` query GetNamespaces { allNamespaces { - id name } } diff --git a/src/nextapp/pages/manager/gateways/index.tsx b/src/nextapp/pages/manager/gateways/index.tsx index bee01eb8f..651f730e1 100644 --- a/src/nextapp/pages/manager/gateways/index.tsx +++ b/src/nextapp/pages/manager/gateways/index.tsx @@ -28,6 +28,7 @@ import NamespaceManager from '@/components/namespace-manager/namespace-manager'; import { Namespace } from '@/shared/types/query.types'; import SearchInput from '@/components/search-input'; import PublishingPopover from '@/components/publishing-popover'; +import { useRouter } from 'next/router'; type GatewayActions = { title: string; @@ -77,6 +78,14 @@ const MyGatewaysPage: React.FC = () => { ); const today = new Date(); + // Redirect to Get Started page if no gateways + const router = useRouter(); + React.useEffect(() => { + if (isSuccess && data.allNamespaces.length == 0) { + router.push('/manager/gateways/get-started'); + } + }, [data]); + // Namespace change const client = useQueryClient(); const toast = useToast(); From f4293f0fd3f30678d80157e87fdb4113a3934ad4 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Thu, 20 Jun 2024 13:26:43 -0700 Subject: [PATCH 069/191] show banner if gateways on Get Started --- .../pages/manager/gateways/get-started.tsx | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/nextapp/pages/manager/gateways/get-started.tsx b/src/nextapp/pages/manager/gateways/get-started.tsx index aba2b9377..804df86fb 100644 --- a/src/nextapp/pages/manager/gateways/get-started.tsx +++ b/src/nextapp/pages/manager/gateways/get-started.tsx @@ -2,9 +2,15 @@ import GatewayGetStarted from '@/components/gateway-get-started'; import PageHeader from '@/components/page-header'; import { useApi } from '@/shared/services/api'; import { + Box, Container, - Heading + Flex, + Icon, + Heading, + Link, + Text } from '@chakra-ui/react'; +import { FaInfoCircle } from 'react-icons/fa'; import { gql } from 'graphql-request'; import Head from 'next/head'; import React from 'react'; @@ -13,7 +19,11 @@ const NamespacesPage: React.FC = () => { const { data, isSuccess, isError } = useApi( 'allNamespaces', { query }, - { suspense: false } + { + suspense: false, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + } ); return ( @@ -23,15 +33,33 @@ const NamespacesPage: React.FC = () => { API Program Services | My Gateways + {isError && ( + Gateways Failed to Load + )} + {(isSuccess && data.allNamespaces.length != 0) && ( + + + + + + + You have gateways. Visit the{' '} + My Gateways page + {' '}to manage them. + + + + + + )} <> - {isError && ( - Gateways Failed to Load - )} - {(isSuccess && data.allNamespaces.length != 0) && ( - Gateways Found! - )} From d499f232a22baf423e490df073ca18e143a1cf54 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Fri, 21 Jun 2024 10:00:36 -0700 Subject: [PATCH 070/191] show all GWs if no recently viewed GWs --- .../components/namespace-menu/namespace-menu.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index 7f63aa904..05b1f9bd6 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -46,7 +46,12 @@ const NamespaceMenu: React.FC = ({ setSearch(value); handleRefresh(); }; - + + const allNamespaces = (data?.allNamespaces || []).sort((a, b) => { + if (a.displayName < b.displayName) return -1; + if (a.displayName > b.displayName) return 1; + return 0; + }); const namespacesRecentlyViewed = JSON.parse(localStorage.getItem('namespacesRecentlyViewed') || '[]'); const recentNamespaces = data?.allNamespaces .filter((namespace: Namespace) => { @@ -181,10 +186,10 @@ const NamespaceMenu: React.FC = ({ ) : ( - recentNamespaces.length > 0 ? + allNamespaces.length > 0 ? ( - Recently viewed + {recentNamespaces.length > 0 ? "Recently viewed" : "Gateways"} ) : null ) @@ -201,7 +206,7 @@ const NamespaceMenu: React.FC = ({ />
    )} - {(search !== '' ? namespaceSearchResults : recentNamespaces).map((n) => ( + {(search !== '' ? namespaceSearchResults : (recentNamespaces.length === 0 ? allNamespaces : recentNamespaces)).map((n) => ( Date: Fri, 21 Jun 2024 10:04:38 -0700 Subject: [PATCH 071/191] show displayName on selector button --- src/nextapp/components/namespace-menu/namespace-menu.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index 05b1f9bd6..218278c23 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -21,15 +21,14 @@ import { gql } from 'graphql-request'; import SearchInput from '@/components/search-input'; import { restApi, useApi } from '@/shared/services/api'; import { Namespace } from '@/shared/types/query.types'; +import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; interface NamespaceMenuProps { user: UserData; - buttonMessage?: string; } const NamespaceMenu: React.FC = ({ user, - buttonMessage, }) => { const client = useQueryClient(); const toast = useToast(); @@ -47,6 +46,7 @@ const NamespaceMenu: React.FC = ({ handleRefresh(); }; + const currentNamespace = useCurrentNamespace(); const allNamespaces = (data?.allNamespaces || []).sort((a, b) => { if (a.displayName < b.displayName) return -1; if (a.displayName > b.displayName) return 1; @@ -136,7 +136,7 @@ const NamespaceMenu: React.FC = ({ justifyContent="space-between" > - {user?.namespace ?? buttonMessage ?? 'No Active Gateway'} + {currentNamespace.data?.currentNamespace.displayName ?? 'No Active Gateway'}
    From 9af68154d0e8c7c035360b07da031079a45cc653 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Mon, 24 Jun 2024 09:54:37 -0700 Subject: [PATCH 072/191] handle null currentNamespace --- src/nextapp/components/namespace-menu/namespace-menu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index 218278c23..dffd45d69 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -136,7 +136,7 @@ const NamespaceMenu: React.FC = ({ justifyContent="space-between" > - {currentNamespace.data?.currentNamespace.displayName ?? 'No Active Gateway'} + {currentNamespace.data?.currentNamespace?.displayName ?? 'No Active Gateway'} From b589ea8c1f4c00eb13c25c8d46d80947cb37df98 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Mon, 24 Jun 2024 09:55:43 -0700 Subject: [PATCH 073/191] link to My Gateways --- src/nextapp/components/namespace-menu/namespace-menu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index dffd45d69..742c0b906 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -235,7 +235,7 @@ const NamespaceMenu: React.FC = ({ Date: Mon, 24 Jun 2024 10:30:52 -0700 Subject: [PATCH 074/191] remove Get Started from nav links --- src/nextapp/shared/data/links.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextapp/shared/data/links.ts b/src/nextapp/shared/data/links.ts index 2165b5163..80389bbdd 100644 --- a/src/nextapp/shared/data/links.ts +++ b/src/nextapp/shared/data/links.ts @@ -57,7 +57,7 @@ const links: NavLink[] = [ name: 'Gateways Get Started', url: '/manager/gateways/get-started', access: ['portal-user'], - sites: ['devportal'], + sites: ['manager'], }, { name: 'Gateways', From 55fa508fbfa3df1835fb07868af2e3ab0d4deede Mon Sep 17 00:00:00 2001 From: James Elson Date: Mon, 24 Jun 2024 10:38:12 -0700 Subject: [PATCH 075/191] use appendedBreadcrumbs to check if it's the gateway details page. Remove check for displayName as it's no longer required. --- .../pages/manager/namespaces/index.tsx | 4 +-- src/nextapp/shared/hooks/index.ts | 1 - .../shared/hooks/use-namespace-breadcrumbs.ts | 18 ++++++------ .../hooks/use-namespace-root-breadcrumbs.ts | 28 ------------------- 4 files changed, 11 insertions(+), 40 deletions(-) delete mode 100644 src/nextapp/shared/hooks/use-namespace-root-breadcrumbs.ts diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index 0d80b6697..633ace8c9 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -60,7 +60,7 @@ import { Namespace, Query } from '@/shared/types/query.types'; import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; import { useGlobal } from '@/shared/services/global'; import EditNamespaceDisplayName from '@/components/edit-display-name'; -import { useNamespaceRootBreadcrumbs } from '@/shared/hooks'; +import { useNamespaceBreadcrumbs } from '@/shared/hooks'; const actions = [ { @@ -121,7 +121,7 @@ const secondaryActions = [ const NamespacesPage: React.FC = () => { const { user } = useAuth(); - const breadcrumbs = useNamespaceRootBreadcrumbs(); + const breadcrumbs = useNamespaceBreadcrumbs(); const hasNamespace = !!user?.namespace; const router = useRouter(); const toast = useToast(); diff --git a/src/nextapp/shared/hooks/index.ts b/src/nextapp/shared/hooks/index.ts index 48f490298..ac3f95bc2 100644 --- a/src/nextapp/shared/hooks/index.ts +++ b/src/nextapp/shared/hooks/index.ts @@ -1,2 +1 @@ export { default as useNamespaceBreadcrumbs } from './use-namespace-breadcrumbs'; -export { default as useNamespaceRootBreadcrumbs } from './use-namespace-root-breadcrumbs'; diff --git a/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts b/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts index db6e0a9ff..5b5c63746 100644 --- a/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts +++ b/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts @@ -10,8 +10,8 @@ const useNamespaceBreadcrumbs = ( ): Breadcrumb[] => { const namespace = useCurrentNamespace(); - if (namespace.isSuccess && !namespace.isFetching) { - if (namespace.data?.currentNamespace?.displayName) { + if (appendedBreadcrumbs) { + if (namespace.isSuccess && !namespace.isFetching) { return [ { href: '/manager/gateways', text: 'My Gateways' }, { @@ -21,18 +21,18 @@ const useNamespaceBreadcrumbs = ( ...appendedBreadcrumbs, ]; } else { + return appendedBreadcrumbs; + } + } else { + if (namespace.isSuccess && !namespace.isFetching) { return [ { href: '/manager/gateways', text: 'My Gateways' }, - { - href: '/manager/namespaces', - text: `${namespace.data?.currentNamespace?.name}`, - }, - ...appendedBreadcrumbs, + { text: `${namespace.data?.currentNamespace?.displayName}` }, ]; + } else { + return [{ href: '/manager/gateways', text: 'My Gateways' }]; } } - - return appendedBreadcrumbs; }; export default useNamespaceBreadcrumbs; diff --git a/src/nextapp/shared/hooks/use-namespace-root-breadcrumbs.ts b/src/nextapp/shared/hooks/use-namespace-root-breadcrumbs.ts deleted file mode 100644 index b094ecffb..000000000 --- a/src/nextapp/shared/hooks/use-namespace-root-breadcrumbs.ts +++ /dev/null @@ -1,28 +0,0 @@ -import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; - -type Breadcrumb = { - href?: string; - text: string; -}; - -const useNamespaceRootBreadcrumbs = (): Breadcrumb[] => { - const namespace = useCurrentNamespace(); - - if (namespace.isSuccess && !namespace.isFetching) { - if (namespace.data?.currentNamespace?.displayName) { - return [ - { href: '/manager/gateways', text: 'My Gateways' }, - { text: `${namespace.data?.currentNamespace?.displayName}` }, - ]; - } else { - return [ - { href: '/manager/gateways', text: 'My Gateways' }, - { text: `${namespace.data?.currentNamespace?.name}` }, - ]; - } - } - - return [{ href: '/manager/gateways', text: 'My Gateways' }]; -}; - -export default useNamespaceRootBreadcrumbs; From 8d0767d49d3c7a0b93f0da1015e35ae6d526c465 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Mon, 24 Jun 2024 12:24:21 -0700 Subject: [PATCH 076/191] don't refetch on search change --- src/nextapp/components/namespace-menu/namespace-menu.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index 742c0b906..425c8e174 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -43,7 +43,6 @@ const NamespaceMenu: React.FC = ({ }; const handleSearchChange = (value: string) => { setSearch(value); - handleRefresh(); }; const currentNamespace = useCurrentNamespace(); From e3c600bafa3c6c4a8912420d150e9de5008d6f9d Mon Sep 17 00:00:00 2001 From: James Elson Date: Mon, 24 Jun 2024 14:33:23 -0700 Subject: [PATCH 077/191] Gateways restructure and redirect logic --- src/nextapp/components/nav-bar/nav-bar.tsx | 43 +-- .../index.tsx => gateways/detail.tsx} | 0 .../pages/manager/gateways/get-started.tsx | 34 +- src/nextapp/pages/manager/gateways/index.tsx | 362 ++---------------- src/nextapp/pages/manager/gateways/list.tsx | 362 ++++++++++++++++++ .../shared/hooks/use-namespace-breadcrumbs.ts | 8 +- 6 files changed, 430 insertions(+), 379 deletions(-) rename src/nextapp/pages/manager/{namespaces/index.tsx => gateways/detail.tsx} (100%) create mode 100644 src/nextapp/pages/manager/gateways/list.tsx diff --git a/src/nextapp/components/nav-bar/nav-bar.tsx b/src/nextapp/components/nav-bar/nav-bar.tsx index 65a5ddac2..488ad8177 100644 --- a/src/nextapp/components/nav-bar/nav-bar.tsx +++ b/src/nextapp/components/nav-bar/nav-bar.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Box, Container, Flex, Link, Text} from '@chakra-ui/react'; +import { Box, Container, Flex, Link, Text } from '@chakra-ui/react'; import NextLink from 'next/link'; import type { NavLink } from '@/shared/data/links'; import NamespaceMenu from '../namespace-menu'; @@ -47,7 +47,7 @@ const NavBar: React.FC = ({ site, links, pathname }) => { return ''; }, [pathname]); - + return ( = ({ site, links, pathname }) => { ))} - {((pathname.startsWith('/manager/') && pathname !== '/manager/gateways/get-started') || pathname === '/devportal/api-directory/your-products') && ( - - - - Gateway selected: - - - - + + + Gateway selected: + + + + )} - ); }; diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/gateways/detail.tsx similarity index 100% rename from src/nextapp/pages/manager/namespaces/index.tsx rename to src/nextapp/pages/manager/gateways/detail.tsx diff --git a/src/nextapp/pages/manager/gateways/get-started.tsx b/src/nextapp/pages/manager/gateways/get-started.tsx index 804df86fb..0a8761bee 100644 --- a/src/nextapp/pages/manager/gateways/get-started.tsx +++ b/src/nextapp/pages/manager/gateways/get-started.tsx @@ -2,13 +2,13 @@ import GatewayGetStarted from '@/components/gateway-get-started'; import PageHeader from '@/components/page-header'; import { useApi } from '@/shared/services/api'; import { - Box, - Container, - Flex, - Icon, - Heading, - Link, - Text + Box, + Container, + Flex, + Icon, + Heading, + Link, + Text, } from '@chakra-ui/react'; import { FaInfoCircle } from 'react-icons/fa'; import { gql } from 'graphql-request'; @@ -29,14 +29,10 @@ const NamespacesPage: React.FC = () => { return ( <> - - API Program Services | My Gateways - + API Program Services | My Gateways - {isError && ( - Gateways Failed to Load - )} - {(isSuccess && data.allNamespaces.length != 0) && ( + {isError && Gateways Failed to Load} + {isSuccess && data.allNamespaces.length != 0 && ( @@ -45,12 +41,14 @@ const NamespacesPage: React.FC = () => { You have gateways. Visit the{' '} My Gateways page - {' '}to manage them. + > + My Gateways page + {' '} + to manage them. @@ -75,4 +73,4 @@ const query = gql` name } } -`; \ No newline at end of file +`; diff --git a/src/nextapp/pages/manager/gateways/index.tsx b/src/nextapp/pages/manager/gateways/index.tsx index 651f730e1..474f2be63 100644 --- a/src/nextapp/pages/manager/gateways/index.tsx +++ b/src/nextapp/pages/manager/gateways/index.tsx @@ -1,362 +1,54 @@ import * as React from 'react'; -import { - Box, - Container, - Icon, - Heading, - Text, - Link, - useDisclosure, - Button, - Flex, - Spacer, - useToast, - Select, - Center, -} from '@chakra-ui/react'; -import Head from 'next/head'; import { gql } from 'graphql-request'; -import { FaPlus, FaLaptopCode, FaRocket, FaServer } from 'react-icons/fa'; -import { useQueryClient } from 'react-query'; -import { differenceInDays } from 'date-fns'; - -import PageHeader from '@/components/page-header'; -import GridLayout from '@/layouts/grid'; -import Card from '@/components/card'; -import { restApi, useApi } from '@/shared/services/api'; -import NamespaceManager from '@/components/namespace-manager/namespace-manager'; -import { Namespace } from '@/shared/types/query.types'; -import SearchInput from '@/components/search-input'; -import PublishingPopover from '@/components/publishing-popover'; import { useRouter } from 'next/router'; -type GatewayActions = { - title: string; - url: string; - urlText: string; - icon: React.ComponentType; - description: string; - descriptionEnd: string; -}; - -const actions: GatewayActions[] = [ - { - title: 'Need to create a new gateway?', - url: - 'https://developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/tutorials/quick-start/', - urlText: 'API Provider Quick Start', - icon: FaPlus, - description: 'Follow our', - descriptionEnd: 'guide.', - }, - { - title: 'GWA CLI commands', - url: - 'https://developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/resources/gwa-commands/', - urlText: 'GWA CLI', - icon: FaLaptopCode, - description: 'Explore helpful commands in our', - descriptionEnd: 'guide.', - }, - { - title: 'Ready to deploy to production?', - url: - 'https://developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/guides/owner-journey-v1/#production-links', - urlText: 'going to production', - icon: FaRocket, - description: 'Check our', - descriptionEnd: 'checklist.', - }, -]; +import { useApi } from '@/shared/services/api'; +import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; -const MyGatewaysPage: React.FC = () => { - const managerDisclosure = useDisclosure(); - const { data, isLoading, isSuccess, isError } = useApi( +const GatewaysHome: React.FC = () => { + const { data, isSuccess, isError } = useApi( 'allNamespaces', { query }, { suspense: false } ); - const today = new Date(); - - // Redirect to Get Started page if no gateways + const namespace = useCurrentNamespace(); const router = useRouter(); React.useEffect(() => { - if (isSuccess && data.allNamespaces.length == 0) { + if (isSuccess && data.allNamespaces.length === 0) { router.push('/manager/gateways/get-started'); } - }, [data]); - - // Namespace change - const client = useQueryClient(); - const toast = useToast(); - const handleNamespaceChange = React.useCallback( - (namespace: Namespace) => async () => { - toast({ - title: `Switching to ${namespace.name} namespace`, - status: 'info', - isClosable: true, - }); - try { - await restApi(`/admin/switch/${namespace.id}`, { method: 'PUT' }); - toast.closeAll(); - client.invalidateQueries(); - toast({ - title: `Switched to ${namespace.name} namespace`, - status: 'success', - isClosable: true, - }); - } catch (err) { - toast.closeAll(); - toast({ - title: 'Unable to switch namespaces', - status: 'error', - isClosable: true, - }); + if (isSuccess && data.allNamespaces.length > 0) { + if ( + namespace.isSuccess && + !namespace.isFetching && + namespace.data.currentNamespace + ) { + router.push('/manager/gateways/detail'); } - }, - [client, toast] - ); - - // Filtering - const [filter, setFilter] = React.useState(''); - const handleFilterChange = React.useCallback( - (event: React.ChangeEvent) => { - setFilter(event.target.value); - }, - [] - ); - - // Search - const [search, setSearch] = React.useState(''); - const handleSearchChange = (value: string) => { - setSearch(value); - }; - - // Filter and search results - const filterBySearch = (result) => { - const regex = new RegExp( - search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'), - 'i' - ); - return result.filter( - (s) => regex.test(s.name) || regex.test(s.displayName) - ); - }; - const namespaceSearchResults = React.useMemo(() => { - const result = data?.allNamespaces ?? []; - if (search.trim()) { - if (filter === 'disabled') { - return filterBySearch(result).filter( - (s) => s.orgEnabled === false && !s.orgUpdatedAt - ); - } else if (filter === 'pending') { - return filterBySearch(result).filter( - (s) => s.orgEnabled === false && s.orgUpdatedAt - ); - } else if (filter === 'enabled') { - return filterBySearch(result).filter((s) => s.orgEnabled === true); - } else { - return filterBySearch(result); + if ( + namespace.isSuccess && + !namespace.isFetching && + !namespace.data.currentNamespace + ) { + router.push('/manager/gateways/list'); } - } else { - if (filter === 'disabled') { - return result.filter((s) => s.orgEnabled === false && !s.orgUpdatedAt); - } else if (filter === 'pending') { - return result.filter((s) => s.orgEnabled === false && s.orgUpdatedAt); - } else if (filter === 'enabled') { - return result.filter((s) => s.orgEnabled === true); - } else { - return result; + if (namespace.isError) { + router.push('/manager/gateways/list'); } } - }, [data, search, filter]); - - return ( - <> - - API Program Services | My Gateways - - - - Export Gateway Report - - } - title="My Gateways" - > - - {actions.map((action) => ( - - - - - - {action.title} - - - - {action.description}{' '} - - {action.urlText} - {' '} - {action.descriptionEnd} - - - - ))} - - - - - event.currentTarget.focus()} - onChange={handleSearchChange} - value={search} - data-testid="namespace-search-input" - /> - - {isSuccess && - (namespaceSearchResults.length === 1 ? ( - {namespaceSearchResults.length} gateway - ) : ( - {namespaceSearchResults.length} gateways - ))} - {isLoading && Loading gateways...} - {isError && Gateways failed to load} - {isSuccess && ( - <> - {namespaceSearchResults.map((namespace) => ( - - - - - - {namespace.displayName - ? namespace.displayName - : namespace.name} - - {differenceInDays( - today, - new Date(namespace.orgUpdatedAt) - ) <= 5 && ( -
    - - New - -
    - )} -
    - - {namespace.name} - -
    - - {namespace.orgEnabled === false && - !namespace.orgUpdatedAt && ( - - )} - {namespace.orgEnabled === false && namespace.orgUpdatedAt && ( - - )} - {namespace.orgEnabled === true && ( - - )} -
    - ))} - {namespaceSearchResults.length === 0 && ( - <> - - Empty folder - - - - No results found - - - - )} - - )} -
    -
    - {data && ( - - )} - - ); + if (isError) { + router.push('/manager/gateways/list'); + } + }, [data, namespace]); + return <>; }; -export default MyGatewaysPage; +export default GatewaysHome; const query = gql` query GetNamespaces { allNamespaces { - id name - displayName - orgEnabled - orgUpdatedAt } } `; diff --git a/src/nextapp/pages/manager/gateways/list.tsx b/src/nextapp/pages/manager/gateways/list.tsx new file mode 100644 index 000000000..db15f785c --- /dev/null +++ b/src/nextapp/pages/manager/gateways/list.tsx @@ -0,0 +1,362 @@ +import * as React from 'react'; +import { + Box, + Container, + Icon, + Heading, + Text, + Link, + useDisclosure, + Button, + Flex, + Spacer, + useToast, + Select, + Center, +} from '@chakra-ui/react'; +import Head from 'next/head'; +import { gql } from 'graphql-request'; +import { FaPlus, FaLaptopCode, FaRocket, FaServer } from 'react-icons/fa'; +import { useQueryClient } from 'react-query'; +import { differenceInDays } from 'date-fns'; + +import PageHeader from '@/components/page-header'; +import GridLayout from '@/layouts/grid'; +import Card from '@/components/card'; +import { restApi, useApi } from '@/shared/services/api'; +import NamespaceManager from '@/components/namespace-manager/namespace-manager'; +import { Namespace } from '@/shared/types/query.types'; +import SearchInput from '@/components/search-input'; +import PublishingPopover from '@/components/publishing-popover'; +import { useRouter } from 'next/router'; + +type GatewayActions = { + title: string; + url: string; + urlText: string; + icon: React.ComponentType; + description: string; + descriptionEnd: string; +}; + +const actions: GatewayActions[] = [ + { + title: 'Need to create a new gateway?', + url: + 'https://developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/tutorials/quick-start/', + urlText: 'API Provider Quick Start', + icon: FaPlus, + description: 'Follow our', + descriptionEnd: 'guide.', + }, + { + title: 'GWA CLI commands', + url: + 'https://developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/resources/gwa-commands/', + urlText: 'GWA CLI', + icon: FaLaptopCode, + description: 'Explore helpful commands in our', + descriptionEnd: 'guide.', + }, + { + title: 'Ready to deploy to production?', + url: + 'https://developer.gov.bc.ca/docs/default/component/aps-infra-platform-docs/guides/owner-journey-v1/#production-links', + urlText: 'going to production', + icon: FaRocket, + description: 'Check our', + descriptionEnd: 'checklist.', + }, +]; + +const MyGatewaysPage: React.FC = () => { + const managerDisclosure = useDisclosure(); + const { data, isLoading, isSuccess, isError } = useApi( + 'allNamespaces', + { query }, + { suspense: false } + ); + const today = new Date(); + + // Redirect to Get Started page if no gateways + const router = useRouter(); + React.useEffect(() => { + if (isSuccess && data.allNamespaces.length === 0) { + router.push('/manager/gateways/get-started'); + } + }, [data]); + + // Namespace change + const client = useQueryClient(); + const toast = useToast(); + const handleNamespaceChange = React.useCallback( + (namespace: Namespace) => async () => { + toast({ + title: `Switching to ${namespace.name} namespace`, + status: 'info', + isClosable: true, + }); + try { + await restApi(`/admin/switch/${namespace.id}`, { method: 'PUT' }); + toast.closeAll(); + client.invalidateQueries(); + toast({ + title: `Switched to ${namespace.name} namespace`, + status: 'success', + isClosable: true, + }); + } catch (err) { + toast.closeAll(); + toast({ + title: 'Unable to switch namespaces', + status: 'error', + isClosable: true, + }); + } + }, + [client, toast] + ); + + // Filtering + const [filter, setFilter] = React.useState(''); + const handleFilterChange = React.useCallback( + (event: React.ChangeEvent) => { + setFilter(event.target.value); + }, + [] + ); + + // Search + const [search, setSearch] = React.useState(''); + const handleSearchChange = (value: string) => { + setSearch(value); + }; + + // Filter and search results + const filterBySearch = (result) => { + const regex = new RegExp( + search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'), + 'i' + ); + return result.filter( + (s) => regex.test(s.name) || regex.test(s.displayName) + ); + }; + const namespaceSearchResults = React.useMemo(() => { + const result = data?.allNamespaces ?? []; + if (search.trim()) { + if (filter === 'disabled') { + return filterBySearch(result).filter( + (s) => s.orgEnabled === false && !s.orgUpdatedAt + ); + } else if (filter === 'pending') { + return filterBySearch(result).filter( + (s) => s.orgEnabled === false && s.orgUpdatedAt + ); + } else if (filter === 'enabled') { + return filterBySearch(result).filter((s) => s.orgEnabled === true); + } else { + return filterBySearch(result); + } + } else { + if (filter === 'disabled') { + return result.filter((s) => s.orgEnabled === false && !s.orgUpdatedAt); + } else if (filter === 'pending') { + return result.filter((s) => s.orgEnabled === false && s.orgUpdatedAt); + } else if (filter === 'enabled') { + return result.filter((s) => s.orgEnabled === true); + } else { + return result; + } + } + }, [data, search, filter]); + + return ( + <> + + API Program Services | My Gateways + + + + Export Gateway Report + + } + title="My Gateways" + > + + {actions.map((action) => ( + + + + + + {action.title} + + + + {action.description}{' '} + + {action.urlText} + {' '} + {action.descriptionEnd} + + + + ))} + + + + + event.currentTarget.focus()} + onChange={handleSearchChange} + value={search} + data-testid="namespace-search-input" + /> + + {isSuccess && + (namespaceSearchResults.length === 1 ? ( + {namespaceSearchResults.length} gateway + ) : ( + {namespaceSearchResults.length} gateways + ))} + {isLoading && Loading gateways...} + {isError && Gateways failed to load} + {isSuccess && ( + <> + {namespaceSearchResults.map((namespace) => ( + + + + + + {namespace.displayName + ? namespace.displayName + : namespace.name} + + {differenceInDays( + today, + new Date(namespace.orgUpdatedAt) + ) <= 5 && ( +
    + + New + +
    + )} +
    + + {namespace.name} + +
    + + {namespace.orgEnabled === false && + !namespace.orgUpdatedAt && ( + + )} + {namespace.orgEnabled === false && namespace.orgUpdatedAt && ( + + )} + {namespace.orgEnabled === true && ( + + )} +
    + ))} + {namespaceSearchResults.length === 0 && ( + <> + + Empty folder + + + + No results found + + + + )} + + )} +
    +
    + {data && ( + + )} + + ); +}; + +export default MyGatewaysPage; + +const query = gql` + query GetNamespaces { + allNamespaces { + id + name + displayName + orgEnabled + orgUpdatedAt + } + } +`; diff --git a/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts b/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts index 5b5c63746..d1a2f00b7 100644 --- a/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts +++ b/src/nextapp/shared/hooks/use-namespace-breadcrumbs.ts @@ -13,9 +13,9 @@ const useNamespaceBreadcrumbs = ( if (appendedBreadcrumbs) { if (namespace.isSuccess && !namespace.isFetching) { return [ - { href: '/manager/gateways', text: 'My Gateways' }, + { href: '/manager/gateways/list', text: 'My Gateways' }, { - href: '/manager/namespaces', + href: '/manager/gateways/detail', text: `${namespace.data?.currentNamespace?.displayName}`, }, ...appendedBreadcrumbs, @@ -26,11 +26,11 @@ const useNamespaceBreadcrumbs = ( } else { if (namespace.isSuccess && !namespace.isFetching) { return [ - { href: '/manager/gateways', text: 'My Gateways' }, + { href: '/manager/gateways/list', text: 'My Gateways' }, { text: `${namespace.data?.currentNamespace?.displayName}` }, ]; } else { - return [{ href: '/manager/gateways', text: 'My Gateways' }]; + return [{ href: '/manager/gateways/list', text: 'My Gateways' }]; } } }; From 6248bae64c0c9c050f20269ce193d3babf43d673 Mon Sep 17 00:00:00 2001 From: James Elson Date: Mon, 24 Jun 2024 14:44:19 -0700 Subject: [PATCH 078/191] My gateways page redirects to detail page when gateway selected --- src/nextapp/pages/manager/gateways/list.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nextapp/pages/manager/gateways/list.tsx b/src/nextapp/pages/manager/gateways/list.tsx index db15f785c..dda5f30d2 100644 --- a/src/nextapp/pages/manager/gateways/list.tsx +++ b/src/nextapp/pages/manager/gateways/list.tsx @@ -105,6 +105,7 @@ const MyGatewaysPage: React.FC = () => { status: 'success', isClosable: true, }); + router.push('/manager/gateways/detail'); } catch (err) { toast.closeAll(); toast({ From 7fea734e183c56aa74367308c1514cb0aa2a78e3 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Mon, 24 Jun 2024 15:36:36 -0700 Subject: [PATCH 079/191] update recentlyViewedNamespaces on change from list --- .../namespace-menu/namespace-menu.tsx | 13 ++--------- src/nextapp/pages/manager/gateways/list.tsx | 5 ++++ src/nextapp/shared/services/utils.ts | 23 +++++++++++++++++-- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index 425c8e174..da0160022 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -22,6 +22,7 @@ import SearchInput from '@/components/search-input'; import { restApi, useApi } from '@/shared/services/api'; import { Namespace } from '@/shared/types/query.types'; import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; +import { updateRecentlyViewedNamespaces } from '@/shared/services/utils'; interface NamespaceMenuProps { user: UserData; @@ -83,17 +84,7 @@ const NamespaceMenu: React.FC = ({ }); try { await restApi(`/admin/switch/${namespace.id}`, { method: 'PUT' }); - const existingEntryIndex = namespacesRecentlyViewed.findIndex((entry: any) => entry.userId === user.userId && entry.namespace === user.namespace); - if (existingEntryIndex !== -1) { - namespacesRecentlyViewed[existingEntryIndex].updatedAt = user.updatedAt; - } else { - namespacesRecentlyViewed.push({ - userId: user.userId, - namespace: user.namespace, - updatedAt: user.updatedAt - }); - } - localStorage.setItem('namespacesRecentlyViewed', JSON.stringify(namespacesRecentlyViewed)); + updateRecentlyViewedNamespaces(namespacesRecentlyViewed, user); handleSearchChange(''); toast.closeAll(); client.invalidateQueries(); diff --git a/src/nextapp/pages/manager/gateways/list.tsx b/src/nextapp/pages/manager/gateways/list.tsx index dda5f30d2..d512cd71c 100644 --- a/src/nextapp/pages/manager/gateways/list.tsx +++ b/src/nextapp/pages/manager/gateways/list.tsx @@ -29,6 +29,8 @@ import { Namespace } from '@/shared/types/query.types'; import SearchInput from '@/components/search-input'; import PublishingPopover from '@/components/publishing-popover'; import { useRouter } from 'next/router'; +import { updateRecentlyViewedNamespaces } from '@/shared/services/utils'; +import { useAuth } from '@/shared/services/auth'; type GatewayActions = { title: string; @@ -70,6 +72,7 @@ const actions: GatewayActions[] = [ ]; const MyGatewaysPage: React.FC = () => { + const { user } = useAuth(); const managerDisclosure = useDisclosure(); const { data, isLoading, isSuccess, isError } = useApi( 'allNamespaces', @@ -89,6 +92,7 @@ const MyGatewaysPage: React.FC = () => { // Namespace change const client = useQueryClient(); const toast = useToast(); + const namespacesRecentlyViewed = JSON.parse(localStorage.getItem('namespacesRecentlyViewed') || '[]'); const handleNamespaceChange = React.useCallback( (namespace: Namespace) => async () => { toast({ @@ -98,6 +102,7 @@ const MyGatewaysPage: React.FC = () => { }); try { await restApi(`/admin/switch/${namespace.id}`, { method: 'PUT' }); + updateRecentlyViewedNamespaces(namespacesRecentlyViewed, user); toast.closeAll(); client.invalidateQueries(); toast({ diff --git a/src/nextapp/shared/services/utils.ts b/src/nextapp/shared/services/utils.ts index 98dd2741f..d179bd024 100644 --- a/src/nextapp/shared/services/utils.ts +++ b/src/nextapp/shared/services/utils.ts @@ -1,8 +1,8 @@ +import { UserData } from '@/types'; import { FaKey, FaLock, - FaLockOpen, - // FaUserSecret, + FaLockOpen } from 'react-icons/fa'; import { IconType } from 'react-icons/lib'; @@ -71,3 +71,22 @@ export const delay = async (timeout = 100): Promise => { setTimeout(resolve, timeout); }); }; + +export const updateRecentlyViewedNamespaces = (namespacesRecentlyViewed: any[], user: UserData) => { + const existingEntryIndex = namespacesRecentlyViewed.findIndex((entry: any) => entry.userId === user.userId && entry.namespace === user.namespace); + + if (existingEntryIndex !== -1) { + // Update existing entry + namespacesRecentlyViewed[existingEntryIndex].updatedAt = user.updatedAt; + } else { + // Add new entry + namespacesRecentlyViewed.push({ + userId: user.userId, + namespace: user.namespace, + updatedAt: user.updatedAt + }); + } + + // Update localStorage + localStorage.setItem('namespacesRecentlyViewed', JSON.stringify(namespacesRecentlyViewed)); +}; From c5804708df7c669fb13ff323c2313563f1a0d7c2 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Mon, 24 Jun 2024 15:59:21 -0700 Subject: [PATCH 080/191] add GRAFANA_URL to .env.local --- src/nextapp/.env.local | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nextapp/.env.local b/src/nextapp/.env.local index 1cb349776..4fdcfcabd 100644 --- a/src/nextapp/.env.local +++ b/src/nextapp/.env.local @@ -1,5 +1,6 @@ NEXT_PUBLIC_APP_VERSION=0.0.0 NEXT_PUBLIC_APP_REVISION=000000000000000000000000 +NEXT_PUBLIC_GRAFANA_URL=https://grafana-apps-gov-bc-ca.dev.api.gov.bc.ca NEXT_PUBLIC_KUBE_CLUSTER=local NEXT_PUBLIC_HELP_DESK_URL=https://dpdd.atlassian.net/servicedesk/customer/portal/1/group/2 NEXT_PUBLIC_HELP_CHAT_URL=https://chat.developer.gov.bc.ca/channel/aps-ops From 01baa0cabaebc781cf186318d6ceb87b7951e166 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Mon, 24 Jun 2024 16:06:05 -0700 Subject: [PATCH 081/191] don't show gw selector on list --- src/nextapp/components/namespace-menu/namespace-menu.tsx | 2 +- src/nextapp/components/nav-bar/nav-bar.tsx | 2 +- src/nextapp/pages/_app.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index da0160022..9a279776a 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -225,7 +225,7 @@ const NamespaceMenu: React.FC = ({ = ({ site, links, pathname }) => {
    {((pathname.startsWith('/manager/') && pathname !== '/manager/gateways/get-started' && - pathname !== '/manager/gateways') || + pathname !== '/manager/gateways/list') || pathname === '/devportal/api-directory/your-products') && ( = ({ Component, pageProps }) => { }, [router]); // Temp solution for handing spacing around new gateways dropdown menu - const gatewaysMenu = ((router?.pathname.startsWith('/manager/') && router?.pathname !== '/manager/gateways/get-started') || router?.pathname === '/devportal/api-directory/your-products') + const gatewaysMenu = ((router?.pathname.startsWith('/manager/') && router?.pathname !== '/manager/gateways/get-started' && router?.pathname !== '/manager/gateways/list') || router?.pathname === '/devportal/api-directory/your-products') if (!queryClientRef.current) { queryClientRef.current = new QueryClient({ From de2078aa68211771ae8cc57172f9594b4993f80b Mon Sep 17 00:00:00 2001 From: James Elson Date: Tue, 25 Jun 2024 16:11:23 -0700 Subject: [PATCH 082/191] Gateways redirect logic optimization --- src/nextapp/components/nav-bar/nav-bar.tsx | 1 + src/nextapp/pages/_app.tsx | 16 +++++- src/nextapp/pages/manager/gateways/index.tsx | 56 +++++++++++--------- 3 files changed, 47 insertions(+), 26 deletions(-) diff --git a/src/nextapp/components/nav-bar/nav-bar.tsx b/src/nextapp/components/nav-bar/nav-bar.tsx index f33eb2467..3858aef4a 100644 --- a/src/nextapp/components/nav-bar/nav-bar.tsx +++ b/src/nextapp/components/nav-bar/nav-bar.tsx @@ -101,6 +101,7 @@ const NavBar: React.FC = ({ site, links, pathname }) => { {((pathname.startsWith('/manager/') && + pathname !== '/manager/gateways' && pathname !== '/manager/gateways/get-started' && pathname !== '/manager/gateways/list') || pathname === '/devportal/api-directory/your-products') && ( diff --git a/src/nextapp/pages/_app.tsx b/src/nextapp/pages/_app.tsx index f15e4719e..6b06ad3b1 100644 --- a/src/nextapp/pages/_app.tsx +++ b/src/nextapp/pages/_app.tsx @@ -70,7 +70,12 @@ const App: React.FC = ({ Component, pageProps }) => { }, [router]); // Temp solution for handing spacing around new gateways dropdown menu - const gatewaysMenu = ((router?.pathname.startsWith('/manager/') && router?.pathname !== '/manager/gateways/get-started' && router?.pathname !== '/manager/gateways/list') || router?.pathname === '/devportal/api-directory/your-products') + const gatewaysMenu = + (router?.pathname.startsWith('/manager/') && + router?.pathname !== '/manager/gateways' && + router?.pathname !== '/manager/gateways/get-started' && + router?.pathname !== '/manager/gateways/list') || + router?.pathname === '/devportal/api-directory/your-products'; if (!queryClientRef.current) { queryClientRef.current = new QueryClient({ @@ -108,7 +113,14 @@ const App: React.FC = ({ Component, pageProps }) => { - + diff --git a/src/nextapp/pages/manager/gateways/index.tsx b/src/nextapp/pages/manager/gateways/index.tsx index 474f2be63..5cdad9e2b 100644 --- a/src/nextapp/pages/manager/gateways/index.tsx +++ b/src/nextapp/pages/manager/gateways/index.tsx @@ -1,9 +1,13 @@ import * as React from 'react'; import { gql } from 'graphql-request'; import { useRouter } from 'next/router'; +import Head from 'next/head'; +import { Container } from '@chakra-ui/react'; import { useApi } from '@/shared/services/api'; -import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; +import { useAuth } from '@/shared/services/auth'; +import PageHeader from '@/components/page-header'; +import { has } from 'lodash'; const GatewaysHome: React.FC = () => { const { data, isSuccess, isError } = useApi( @@ -11,36 +15,40 @@ const GatewaysHome: React.FC = () => { { query }, { suspense: false } ); - const namespace = useCurrentNamespace(); + const { user } = useAuth(); + const hasNamespace = !!user?.namespace; const router = useRouter(); + React.useEffect(() => { - if (isSuccess && data.allNamespaces.length === 0) { - router.push('/manager/gateways/get-started'); - } - if (isSuccess && data.allNamespaces.length > 0) { - if ( - namespace.isSuccess && - !namespace.isFetching && - namespace.data.currentNamespace - ) { - router.push('/manager/gateways/detail'); + if (hasNamespace) { + router.push('/manager/gateways/detail'); + } else { + if (isSuccess && data.allNamespaces.length === 0) { + router.push('/manager/gateways/get-started'); } - if ( - namespace.isSuccess && - !namespace.isFetching && - !namespace.data.currentNamespace - ) { + if (isSuccess && data.allNamespaces.length > 0) { router.push('/manager/gateways/list'); } - if (namespace.isError) { - router.push('/manager/gateways/list'); + if (isError) { + router.push('/manager/gateways/get-started'); } } - if (isError) { - router.push('/manager/gateways/list'); - } - }, [data, namespace]); - return <>; + }, [data, hasNamespace]); + + return ( + <> + {!hasNamespace ? ( + <> + + API Program Services | My Gateways + + + + + + ) : null} + + ); }; export default GatewaysHome; From c8d7e3d8a83a52bffe8e1c7473e781c652e681f2 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Tue, 25 Jun 2024 16:42:10 -0700 Subject: [PATCH 083/191] add details to 'test your gateway' step --- .../gateway-get-started.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/nextapp/components/gateway-get-started/gateway-get-started.tsx b/src/nextapp/components/gateway-get-started/gateway-get-started.tsx index bc39add1e..5d36028d4 100644 --- a/src/nextapp/components/gateway-get-started/gateway-get-started.tsx +++ b/src/nextapp/components/gateway-get-started/gateway-get-started.tsx @@ -334,14 +334,24 @@ const GatewayGetStarted: React.FC = () => { title='Apply your configuration' description='With this command you can apply your configuration to your recently created gateway. If you need to make updates or republish, simply run this command again.' - command='gwa apply --input ' + command='gwa apply --input gw-config.yaml' /> + Visit the{' '} + Your Products page to request credentials. + Then, get the URL for your newly published gateway service ...api.gov.bc.ca using this command. + Pass the client ID and secret in a POST request to this URL to get a JWT token to access your API. + + } + command='gwa status -hosts' /> Help From 32604f9489150383664a7fa5a50850742088efe0 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Tue, 25 Jun 2024 17:24:29 -0700 Subject: [PATCH 084/191] update labels in /nextapp --- .../components/link-consumer/link-consumer.tsx | 2 +- .../namespace-access/namespace-access-dialog.tsx | 4 ++-- .../namespace-actions/namespace-actions.tsx | 4 ++-- .../namespace-delete/namespace-delete.tsx | 8 ++++---- .../namespace-manager/export-report.tsx | 2 +- .../components/namespace-menu/namespace-menu.tsx | 10 +++++----- .../components/new-namespace/new-namespace.tsx | 12 ++++++------ .../new-organization-form.tsx | 8 ++++---- .../components/ns-breadcrumb/ns-breadcrumb.tsx | 2 +- .../components/preview-banner/preview-banner.tsx | 8 ++++---- src/nextapp/pages/index.tsx | 4 ++-- src/nextapp/pages/manager/activity/index.tsx | 4 ++-- .../manager/authorization-profiles/index.tsx | 2 +- src/nextapp/pages/manager/gateways/detail.tsx | 16 ++++++++-------- src/nextapp/pages/manager/gateways/list.tsx | 6 +++--- .../pages/manager/namespace-access/index.tsx | 8 ++++---- 16 files changed, 50 insertions(+), 50 deletions(-) diff --git a/src/nextapp/components/link-consumer/link-consumer.tsx b/src/nextapp/components/link-consumer/link-consumer.tsx index 5e5a764d4..4e6bed450 100644 --- a/src/nextapp/components/link-consumer/link-consumer.tsx +++ b/src/nextapp/components/link-consumer/link-consumer.tsx @@ -38,7 +38,7 @@ const LinkConsumerDialog: React.FC = ({ const client = useQueryClient(); const link = useApiMutation(mutation); const toast = useToast(); - const title = 'Link Consumer to Namespace'; + const title = 'Link Consumer to Gateway'; const formRef = React.useRef(); const { isOpen, onClose, onOpen } = useDisclosure(); const handleLink = async () => { diff --git a/src/nextapp/components/namespace-access/namespace-access-dialog.tsx b/src/nextapp/components/namespace-access/namespace-access-dialog.tsx index 46e8ad2bd..88ed39947 100644 --- a/src/nextapp/components/namespace-access/namespace-access-dialog.tsx +++ b/src/nextapp/components/namespace-access/namespace-access-dialog.tsx @@ -201,6 +201,6 @@ const permissionHelpTextLookup = { 'GatewayConfig.Publish': 'Can publish gateway configuration to Kong and to view the status of the upstreams.', 'Namespace.Manage': - 'Can update the Access Control List for controlling access to viewing metrics, service configuration and service account management. This is a superuser for the Namespace.', - 'Namespace.View': 'Read-only access to the namespace.', + 'Can update the Access Control List for controlling access to viewing metrics, service configuration and service account management. This is a superuser for the gateway.', + 'Namespace.View': 'Read-only access to the gateway.', }; diff --git a/src/nextapp/components/namespace-actions/namespace-actions.tsx b/src/nextapp/components/namespace-actions/namespace-actions.tsx index d91be009a..2cae51e9f 100644 --- a/src/nextapp/components/namespace-actions/namespace-actions.tsx +++ b/src/nextapp/components/namespace-actions/namespace-actions.tsx @@ -33,13 +33,13 @@ const NamespaceActions: React.FC = ({ name }) => { } variant="primary" /> } onClick={handleDelete}> - Delete Namespace... + Delete Gateway... diff --git a/src/nextapp/components/namespace-delete/namespace-delete.tsx b/src/nextapp/components/namespace-delete/namespace-delete.tsx index b563a031d..286a258f8 100644 --- a/src/nextapp/components/namespace-delete/namespace-delete.tsx +++ b/src/nextapp/components/namespace-delete/namespace-delete.tsx @@ -45,7 +45,7 @@ const NamespaceDelete: React.FC = ({ } toast({ - title: ' Namespace deleted', + title: ' Gateway deleted', status: 'success', isClosable: true, }); @@ -53,7 +53,7 @@ const NamespaceDelete: React.FC = ({ onClose(); } catch (err) { toast({ - title: 'Namespace delete failed', + title: 'Gateway delete failed', description: err, status: 'error', isClosable: true, @@ -76,10 +76,10 @@ const NamespaceDelete: React.FC = ({ > - {`Delete ${name} Namespace`} + {`Delete ${name} Gateway`} - {`Are you sure you want to delete the ${name} namespace? It cannot be undone.`} + {`Are you sure you want to delete the ${name} gateway? It cannot be undone.`} diff --git a/src/nextapp/components/new-organization-form/new-organization-form.tsx b/src/nextapp/components/new-organization-form/new-organization-form.tsx index 725cf4ab6..5f77c1eb2 100644 --- a/src/nextapp/components/new-organization-form/new-organization-form.tsx +++ b/src/nextapp/components/new-organization-form/new-organization-form.tsx @@ -63,12 +63,12 @@ const NewOrganizationForm: React.FC = () => { onClose(); toast({ status: 'success', - title: 'Namespace updated', + title: 'Gateway updated', }); } catch (err) { toast({ status: 'error', - title: 'Namespace update failed', + title: 'Gateway update failed', description: err, }); } @@ -88,9 +88,9 @@ const NewOrganizationForm: React.FC = () => { - Adding your Organization and Business Unit to your namespace will + Adding your Organization and Business Unit to your gateway will notify the Organization Administrator to enable API publishing to - the Directory for your namespace so consumers can find and request + the Directory for your gateway so consumers can find and request access to your APIs. diff --git a/src/nextapp/components/ns-breadcrumb/ns-breadcrumb.tsx b/src/nextapp/components/ns-breadcrumb/ns-breadcrumb.tsx index fa2166110..9ae298de3 100644 --- a/src/nextapp/components/ns-breadcrumb/ns-breadcrumb.tsx +++ b/src/nextapp/components/ns-breadcrumb/ns-breadcrumb.tsx @@ -7,7 +7,7 @@ const Breadcrumb = (crumbs = []) => { return user ? [ - { href: '/manager/namespaces', text: `Namespaces (${user.namespace})` }, + { href: '/manager/gateways', text: `Gateways (${user.namespace})` }, ].concat(crumbs) : []; }; diff --git a/src/nextapp/components/preview-banner/preview-banner.tsx b/src/nextapp/components/preview-banner/preview-banner.tsx index dacbc1fa1..df1764385 100644 --- a/src/nextapp/components/preview-banner/preview-banner.tsx +++ b/src/nextapp/components/preview-banner/preview-banner.tsx @@ -82,7 +82,7 @@ const PreviewBanner: React.FC = () => { {`Your Organization Administrator has been notified to enable API - Publishing to the Directory for the ${user.namespace} namespace.`} + Publishing to the Directory for the ${user.namespace} gateway.`}
    - } - title="My Gateways" - > + + + Export Gateway Report + + } + title="My Gateways" + > + {actions.map((action) => ( - + {action.title} From 9b1d320c6ba6656355a1aefcccb1fcad540a86b8 Mon Sep 17 00:00:00 2001 From: James Elson Date: Wed, 26 Jun 2024 15:06:26 -0700 Subject: [PATCH 090/191] Remove New tag --- src/nextapp/pages/manager/gateways/list.tsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/nextapp/pages/manager/gateways/list.tsx b/src/nextapp/pages/manager/gateways/list.tsx index b653aa00b..c5d3ab7cf 100644 --- a/src/nextapp/pages/manager/gateways/list.tsx +++ b/src/nextapp/pages/manager/gateways/list.tsx @@ -79,7 +79,6 @@ const MyGatewaysPage: React.FC = () => { { query }, { suspense: false } ); - const today = new Date(); // Redirect to Get Started page if no gateways const router = useRouter(); @@ -280,23 +279,6 @@ const MyGatewaysPage: React.FC = () => { ? namespace.displayName : namespace.name} - {differenceInDays( - today, - new Date(namespace.orgUpdatedAt) - ) <= 5 && ( -
    - - New - -
    - )} {namespace.name} From 0ae06200775a4aeeb5656a1cc466407f7d970754 Mon Sep 17 00:00:00 2001 From: James Elson Date: Wed, 26 Jun 2024 15:19:56 -0700 Subject: [PATCH 091/191] Detail page: redirect to list if no gateway selected --- src/nextapp/pages/manager/gateways/detail.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/nextapp/pages/manager/gateways/detail.tsx b/src/nextapp/pages/manager/gateways/detail.tsx index 633ace8c9..7a4e0207e 100644 --- a/src/nextapp/pages/manager/gateways/detail.tsx +++ b/src/nextapp/pages/manager/gateways/detail.tsx @@ -156,6 +156,13 @@ const NamespacesPage: React.FC = () => { }; }, [namespace]); + // Redirect to My Gateways page if no gateway selected + React.useEffect(() => { + if (!hasNamespace) { + router.push('/manager/gateways/list'); + } + }, [hasNamespace]); + const handleDelete = React.useCallback(async () => { if (user?.namespace) { try { @@ -274,10 +281,12 @@ const NamespacesPage: React.FC = () => { - + {hasNamespace && ( + + )} <> {isError && Gateways Failed to Load} {isSuccess && data.allNamespaces.length == 0 && } From b158c9290ccf32f280c0c5a2484d7f6660a4c30b Mon Sep 17 00:00:00 2001 From: James Elson Date: Wed, 26 Jun 2024 15:31:58 -0700 Subject: [PATCH 092/191] Naming updates --- src/nextapp/components/namespace-menu/namespace-menu.tsx | 6 +++--- src/nextapp/pages/manager/gateways/list.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index 9a279776a..007c60f1f 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -78,7 +78,7 @@ const NamespaceMenu: React.FC = ({ const handleNamespaceChange = React.useCallback( (namespace: Namespace) => async () => { toast({ - title: `Switching to ${namespace.name} namespace`, + title: `Switching to ${namespace.name} gateway`, status: 'info', isClosable: true, }); @@ -89,14 +89,14 @@ const NamespaceMenu: React.FC = ({ toast.closeAll(); client.invalidateQueries(); toast({ - title: `Switched to ${namespace.name} namespace`, + title: `Switched to ${namespace.name} gateway`, status: 'success', isClosable: true, }); } catch (err) { toast.closeAll(); toast({ - title: 'Unable to switch namespaces', + title: 'Unable to switch gateways', status: 'error', isClosable: true, }); diff --git a/src/nextapp/pages/manager/gateways/list.tsx b/src/nextapp/pages/manager/gateways/list.tsx index c5d3ab7cf..b74d26389 100644 --- a/src/nextapp/pages/manager/gateways/list.tsx +++ b/src/nextapp/pages/manager/gateways/list.tsx @@ -97,7 +97,7 @@ const MyGatewaysPage: React.FC = () => { const handleNamespaceChange = React.useCallback( (namespace: Namespace) => async () => { toast({ - title: `Switching to ${namespace.name} namespace`, + title: `Switching to ${namespace.name} gateway`, status: 'info', isClosable: true, }); @@ -107,7 +107,7 @@ const MyGatewaysPage: React.FC = () => { toast.closeAll(); client.invalidateQueries(); toast({ - title: `Switched to ${namespace.name} namespace`, + title: `Switched to ${namespace.name} gateway`, status: 'success', isClosable: true, }); @@ -115,7 +115,7 @@ const MyGatewaysPage: React.FC = () => { } catch (err) { toast.closeAll(); toast({ - title: 'Unable to switch namespaces', + title: 'Unable to switch gateways', status: 'error', isClosable: true, }); From bd7ada84313b0789ec0688697f978148a2703524 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Wed, 26 Jun 2024 15:57:41 -0700 Subject: [PATCH 093/191] Remove unused POC content --- .../pages/devportal/poc/access/[id].tsx | 165 ------- .../pages/devportal/poc/access/index.tsx | 112 ----- .../pages/devportal/poc/access/list.tsx | 347 ------------- .../pages/devportal/poc/access/queries.js | 120 ----- .../pages/devportal/poc/requests/new/[id].tsx | 160 ------ .../pages/devportal/poc/requests/queries.js | 43 -- .../devportal/poc/requests/request.css.ts | 74 --- .../poc/resources/[credIssuerId]/[id].tsx | 284 ----------- .../pages/devportal/poc/resources/index.tsx | 112 ----- .../devportal/poc/resources/permissions.tsx | 70 --- .../pages/devportal/poc/resources/queries.js | 133 ----- .../devportal/poc/resources/resource-list.tsx | 91 ---- .../devportal/poc/resources/resources.tsx | 65 --- .../devportal/poc/resources/scope-item.tsx | 15 - .../poc/resources/service-accounts.tsx | 59 --- .../pages/devportal/poc/resources/waiting.tsx | 75 --- .../pages/devportal/requests/new/[id].tsx | 454 ------------------ .../pages/devportal/requests/new/tokens.tsx | 197 -------- .../pages/manager/poc/activity/index.tsx | 85 ---- .../pages/manager/poc/activity/list.tsx | 55 --- .../pages/manager/poc/activity/queries.js | 16 - .../pages/platform/poc/applications/index.tsx | 90 ---- .../pages/platform/poc/applications/list.tsx | 67 --- .../platform/poc/applications/queries.js | 26 - src/nextapp/pages/poc/my-profile/index.tsx | 37 -- src/nextapp/shared/data/links.ts | 31 -- 26 files changed, 2983 deletions(-) delete mode 100644 src/nextapp/pages/devportal/poc/access/[id].tsx delete mode 100644 src/nextapp/pages/devportal/poc/access/index.tsx delete mode 100644 src/nextapp/pages/devportal/poc/access/list.tsx delete mode 100644 src/nextapp/pages/devportal/poc/access/queries.js delete mode 100644 src/nextapp/pages/devportal/poc/requests/new/[id].tsx delete mode 100644 src/nextapp/pages/devportal/poc/requests/queries.js delete mode 100644 src/nextapp/pages/devportal/poc/requests/request.css.ts delete mode 100644 src/nextapp/pages/devportal/poc/resources/[credIssuerId]/[id].tsx delete mode 100644 src/nextapp/pages/devportal/poc/resources/index.tsx delete mode 100644 src/nextapp/pages/devportal/poc/resources/permissions.tsx delete mode 100644 src/nextapp/pages/devportal/poc/resources/queries.js delete mode 100644 src/nextapp/pages/devportal/poc/resources/resource-list.tsx delete mode 100644 src/nextapp/pages/devportal/poc/resources/resources.tsx delete mode 100644 src/nextapp/pages/devportal/poc/resources/scope-item.tsx delete mode 100644 src/nextapp/pages/devportal/poc/resources/service-accounts.tsx delete mode 100644 src/nextapp/pages/devportal/poc/resources/waiting.tsx delete mode 100644 src/nextapp/pages/devportal/requests/new/[id].tsx delete mode 100644 src/nextapp/pages/devportal/requests/new/tokens.tsx delete mode 100644 src/nextapp/pages/manager/poc/activity/index.tsx delete mode 100644 src/nextapp/pages/manager/poc/activity/list.tsx delete mode 100644 src/nextapp/pages/manager/poc/activity/queries.js delete mode 100644 src/nextapp/pages/platform/poc/applications/index.tsx delete mode 100644 src/nextapp/pages/platform/poc/applications/list.tsx delete mode 100644 src/nextapp/pages/platform/poc/applications/queries.js delete mode 100644 src/nextapp/pages/poc/my-profile/index.tsx diff --git a/src/nextapp/pages/devportal/poc/access/[id].tsx b/src/nextapp/pages/devportal/poc/access/[id].tsx deleted file mode 100644 index b7ac4c830..000000000 --- a/src/nextapp/pages/devportal/poc/access/[id].tsx +++ /dev/null @@ -1,165 +0,0 @@ -import * as React from 'react'; -import { - Alert, - AlertTitle, - AlertDescription, - AlertIcon, - Button, - Box, - Container, - Divider, - Heading, - Icon, - Stack, - VStack, - Skeleton, - CloseButton, -} from '@chakra-ui/react'; -import Head from 'next/head'; -import PageHeader from '@/components/page-header'; - -import { useDisclosure } from '@chakra-ui/react'; - -import { GET_LIST, GET_REQUEST, GEN_CREDENTIAL } from './queries'; - -import { FaPlusCircle, FaFolder, FaFolderOpen } from 'react-icons/fa'; - -import { useAppContext } from '@/pages/context'; - -const { useEffect, useState } = React; - -import { styles } from '@/shared/styles/devportal.css'; - -import ReactMarkdownWithHtml from 'react-markdown/with-html'; -import gfm from 'remark-gfm'; - -import graphql from '@/shared/services/graphql'; - -import tmpstyles from '../../../docs/docs.module.css'; - -import List from './list'; - -import EnvironmentBadge from '@/components/environment-badge'; - -import ViewSecret from '@/components/view-secret'; - -const customStyles = { - content: { - top: '30%', - left: '20%', - right: '20%', - bottom: 'auto', - transformx: 'translate(-50%, -50%)', - }, - overlay: {}, -}; - -const MyApplicationsPage = () => { - const context = useAppContext(); - - const [request, setRequest] = useState(null); - - const [{ state, data }, setState] = useState({ - state: 'loading', - data: null, - }); - - const fetch = () => { - const { - router: { - pathname, - query: { id }, - }, - } = context; - - if (context['router'] != null && id) { - generateCredential(id) - .then(() => { - graphql(GET_LIST, {}).then(({ data }) => { - setState({ state: 'loaded', data }); - setRequest(data.allAccessRequests.filter((r) => r.id == id)[0]); - }); - }) - .catch((err) => { - setState({ state: 'error', data: null }); - }); - } - // if (context['router'] != null && id) { - - // graphql(GET_REQUEST, { id: id }).then(data => { - // setRequestDetails(data) - // }); - // } - }; - - const [{ cred, reqId }, setCred] = useState({ cred: null, reqId: null }); - - const onSecretClose = () => { - window.location.href = `/devportal/access`; - }; - - const generateCredential = (reqId) => { - return graphql(GEN_CREDENTIAL, { id: reqId }).then((data) => { - if (data.data.updateAccessRequest.credential != 'NEW') { - setCred({ - cred: JSON.parse(data.data.updateAccessRequest.credential), - reqId: reqId, - }); - } - }); - }; - - useEffect(fetch, [context]); - - const cancelRequest = (id) => {}; - - const { isOpen, onOpen, onClose } = useDisclosure(); - - const actions = []; - return ( - <> - - API Program Services | API Access - - - - - - List of the BC Government Service APIs that you have access to. - - - - - - - {request != null && cred != null && ( - - - - - {request.productEnvironment?.product.name} - - - - - -
    - -
    -
    -
    - )} - - {/* */} -
    -
    - - ); -}; - -export default MyApplicationsPage; diff --git a/src/nextapp/pages/devportal/poc/access/index.tsx b/src/nextapp/pages/devportal/poc/access/index.tsx deleted file mode 100644 index aa68a8efb..000000000 --- a/src/nextapp/pages/devportal/poc/access/index.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import * as React from 'react'; -import { - Alert, - AlertIcon, - Button, - Box, - Container, - Stack, - VStack, - Skeleton, - } from '@chakra-ui/react'; -import Head from 'next/head'; -import PageHeader from '@/components/page-header'; - -import { - useDisclosure - } from "@chakra-ui/react" - -import EmptyPane from '@/components/empty-pane'; - -import { GET_LIST, CANCEL_ACCESS } from './queries' - -//import { useAppContext } from '@/pages/context' - -const { useEffect, useState } = React - -import { styles } from '@/shared/styles/devportal.css' - -import graphql from '@/shared/services/graphql' - -import List from './list' - -const customStyles = { - content : { - top : '30%', - left : '20%', - right : '20%', - bottom : 'auto', - transformx : 'translate(-50%, -50%)' - }, - overlay: { - } -}; - -const MyApplicationsPage = () => { - - const [{ state, data}, setState] = useState({ state: 'loading', data: null }); - - const fetch = () => { - graphql(GET_LIST, {}) - .then(({ data }) => { - setState({ state: 'loaded', data }); - }) - .catch((err) => { - setState({ state: 'error', data: null }); - }); - }; - - useEffect(fetch, []); - - const cancelRequest = (id) => { - graphql(CANCEL_ACCESS, {id}) - .then(({ data }) => { - fetch() - }) - .catch((err) => { - console.log(err) - fetch() - }); - }; - - const { isOpen, onOpen, onClose } = useDisclosure() - - const actions = [ - ] - return ( - <> - - API Program Services | API Access - - - - - - List of the BC Government Service APIs that you have access to. - - - - - - - - - { data && data.allServiceAccesses.filter(s => s.productEnvironment != null).length == 0 && ( - - - - )} - - - - - - - ) -} - -export default MyApplicationsPage; diff --git a/src/nextapp/pages/devportal/poc/access/list.tsx b/src/nextapp/pages/devportal/poc/access/list.tsx deleted file mode 100644 index 0e0e450dd..000000000 --- a/src/nextapp/pages/devportal/poc/access/list.tsx +++ /dev/null @@ -1,347 +0,0 @@ -import * as React from 'react'; - -import { styles } from '@/shared/styles/devportal.css'; - -import graphql from '@/shared/services/graphql'; - -import { GEN_CREDENTIAL } from './queries'; - -import { - Alert, - AlertDescription, - AlertTitle, - AlertIcon, - Box, - Badge, - Icon, - CloseButton, - Divider, - Heading, - Input, - InputGroup, - InputRightElement, - Link, - Stack, - Tag, - TagLabel, - Button, - ButtonGroup, - Text, - Spacer, -} from '@chakra-ui/react'; -import { - HStack, - Table, - Thead, - Tbody, - Tr, - Th, - Td, - TableCaption, - VStack, -} from '@chakra-ui/react'; - -import { Tabs, TabList, TabPanels, Tab, TabPanel } from '@chakra-ui/react'; - -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { dark } from 'react-syntax-highlighter/dist/esm/styles/prism'; - -import { FaPlusCircle, FaFolder, FaFolderOpen } from 'react-icons/fa'; - -import ReactMarkdownWithHtml from 'react-markdown/with-html'; -import gfm from 'remark-gfm'; - -import EnvironmentBadge from '@/components/environment-badge'; - -import NameValue from '@/components/name-value'; - -import tmpstyles from '../../docs/docs.module.css'; - -const { useEffect, useState } = React; - -const TEMPL_API_KEY_CURL = ` -# Use the API Key that was generated -# for you when requesting access. -curl -v /status -H "X-API-KEY: " -`; - -const TEMPL_CLIENT_CURL = ` -# Use the Client Creds that was generated -# for you when requesting access. -export TOKEN_ENDPOINT="" -export CLIENT_ID="" -export CLIENT_SECRET="" - -curl -X POST $TOKEN_ENDPOINT \\ - -d grant_type=client_credentials \\ - -d client_id=$CLIENT_ID \\ - -d client_secret=$CLIENT_SECRET \\ - -d scope=openid - -# Extract the token -export TOK="" - -curl -v /status -H "Authorization: Bearer $TOK" - -`; - -const TEMPL_API_KEY_RESTISH = ` -# Use the API Key that was generated -# for you when requesting access. - -restish api config myapi - -? Base URI : - -> Add Header - -Header name: X-API-KEY -Header value: - -> Finished with Profile -> Save and exit - -restish get myapi/ -`; - -const TEMPL_CLIENT_RESTISH = ` -restish api config myapi - -? Base URI : - -> Setup auth - -Select 'oauth-client-credentials' - -Auth parameter client_id: -Auth parameter client_secret: -Auth parameter token_url: -Auth parameter scopes: openid -Add additional auth param? N - -> Finished with profile -> Save and exit - -restish get myapi/ -`; - -const TEMPLATES = { - 'kong-acl-only': [TEMPL_API_KEY_CURL, TEMPL_API_KEY_RESTISH], - 'kong-api-key-acl': [TEMPL_API_KEY_CURL, TEMPL_API_KEY_RESTISH], - 'client-credentials': [TEMPL_CLIENT_CURL, TEMPL_CLIENT_RESTISH], -}; -function List({ data, state, refetch, cancelRequest }) { - switch (state) { - case 'loading': { - return

    Loading...

    ; - } - case 'error': { - return

    Error!

    ; - } - case 'loaded': { - if (!data) { - return

    Ooops, something went wrong!

    ; - } - const products = [ - ...new Set( - data.myServiceAccesses - .map((item, index) => item.productEnvironment?.product.name) - .filter((p) => p != null) - ), - ]; - - return ( - <> - {products.sort().map((product) => ( - - - - - {product} - - - - - - - - - - - - - - {data.myServiceAccesses - .filter( - (access) => - access.productEnvironment?.product.name == product - ) - .map((item, index) => { - const [show, setShow] = React.useState(false); - - return ( - <> - - - - - - {item.active == true && show ? ( - - - - ) : ( - false - )} - - ); - })} - -
    EnvironmentEndpoints
    - - - {item.productEnvironment.name} - - - - {item.productEnvironment.services.map((svc) => { - return svc.routes.map((route) => { - const _methods = JSON.parse(route['methods']); - const methods = - Array.isArray(_methods) && - _methods.length > 0 - ? _methods - : ['ALL']; - const hosts = Array.isArray( - JSON.parse(route['hosts']) - ) - ? JSON.parse(route['hosts']) - : []; - const paths = Array.isArray( - JSON.parse(route['paths']) - ) - ? JSON.parse(route['paths']) - : ['/']; - const hostPaths = hosts.map((h) => - paths.map((p) => `https://${h}${p}`) - ); - return ( - - {hostPaths.map((hp) => ( - <> - - {methods.map((p) => ( - - {p} - - ))} - - {hp} - - ))} - - ); - }); - })} - - - {item.active == false && ( - - PENDING APPROVAL - - )} - - - {item.active == true && ( - - )} - -
    - - - Curl - Restish - Swagger Console - Postman - - - - - - { - TEMPLATES[ - item.productEnvironment.flow - ][0] - } - - - - - { - TEMPLATES[ - item.productEnvironment.flow - ][1] - } - - - -

    Coming soon!

    -
    - -

    Coming soon!

    -
    -
    -
    -
    -
    - ))} - - ); - } - } - return <>; -} - -export default List; diff --git a/src/nextapp/pages/devportal/poc/access/queries.js b/src/nextapp/pages/devportal/poc/access/queries.js deleted file mode 100644 index 409a0d3e1..000000000 --- a/src/nextapp/pages/devportal/poc/access/queries.js +++ /dev/null @@ -1,120 +0,0 @@ -export const GET_LIST = ` - query GET { - allTemporaryIdentities { - id - userId - } - myServiceAccesses(where: { }) { - id - name - active - application { - appId - } - productEnvironment { - name - flow - credentialIssuer { - instruction - } - product { - name - } - services { - name - routes { - name - hosts - methods - paths - } - } - } - } - allAccessRequests(where: { isComplete: null }) { - id - name - isIssued - application { - appId - } - productEnvironment { - name - flow - credentialIssuer { - instruction - } - product { - name - } - services { - name - routes { - name - hosts - methods - paths - } - } - } - } - } -` - -export const GET_REQUEST = ` - query GetRequestById($requestId: ID!) { - allAccessRequests(where: { id: $requestId }) { - id - name - isIssued - application { - appId - } - productEnvironment { - name - credentialIssuer { - instruction - } - product { - name - } - services { - name - routes { - name - hosts - methods - paths - } - } - } - } - } -` -// export const GEN_CREDENTIAL = ` -// mutation GenCredential($id: ID!) { -// updateServiceAccess(id: $id, data: { credential: "NEW" }) { -// credential -// } -// } -// ` - -export const GEN_CREDENTIAL = ` - mutation GenCredential($id: ID!) { - updateAccessRequest(id: $id, data: { credential: "NEW" }) { - credential - } - } -` - -export const CANCEL_ACCESS = ` - mutation CancelAccess($id: ID!) { - deleteServiceAccess(id: $id) { - id - } - } -` - - -const empty = () => false -export default empty diff --git a/src/nextapp/pages/devportal/poc/requests/new/[id].tsx b/src/nextapp/pages/devportal/poc/requests/new/[id].tsx deleted file mode 100644 index c3b025cdd..000000000 --- a/src/nextapp/pages/devportal/poc/requests/new/[id].tsx +++ /dev/null @@ -1,160 +0,0 @@ -import * as React from 'react'; -import { - Alert, - AlertIcon, - Box, - Container, - VStack, - Skeleton, - } from '@chakra-ui/react'; -import Head from 'next/head'; -import PageHeader from '@/components/page-header'; - -import { useRouter } from 'next/router' - -const { useEffect, useState } = React; - -import { ADD, GET_PRODUCT } from './../queries' - -import { styles } from '@/shared/styles/devportal.css'; - -import graphql from '@/shared/services/graphql' - -import NameValue from '@/components/name-value'; - -import { Button, ButtonGroup, Link, Textarea, RadioGroup, Radio, Stack, Flex } from "@chakra-ui/react" - -import { useAppContext } from '@/pages/context' - -import { create } from 'domain'; - -const NewRequest = () => { - const context = useAppContext() - - const [environmentId, setEnvironmentId] = useState(); - const [applicationId, setApplicationId] = useState(); - - const [{ state, data }, setState] = useState({ state: 'loading', data: null }); - const fetch = () => { - const { router: { pathname, query: { id } } } = context - if (context['router'] != null && id) { - graphql(GET_PRODUCT, { id : id }) - .then(({ data }) => { - setState({ state: 'loaded', data }); - }) - .catch((err) => { - setState({ state: 'error', data: null }); - }); - } - }; - useEffect(fetch, [context]); - - const requestor = (data ? data.allTemporaryIdentities[0] : null) - const dataset = (data ? data.allProducts[0] : data) - - const refetch = (data) => { - window.location.href = `/devportal/poc/access/${data.data.createAccessRequest.id}` - } - - - const create = () => { - graphql(ADD, { name: dataset.name + " FOR " + data.allTemporaryIdentities[0].name, controls: "{}", requestor: data.allTemporaryIdentities[0].userId, applicationId: applicationId, productEnvironmentId: environmentId }).then(refetch); - } - //onChange={(a) => { setApplicationId(a) } } value={applicationId} - //onChange={setEnvironmentId} value={environmentId} - return ( - <> - - API Program Services | Request Access - - - - - - - -
    - - { dataset == null ? false: ( - <> - { data.allApplications == null || data.allApplications.length == 0 ? ( - To get started, you must Register an Application first. - ) : false } - { data.allApplications != null && data.allApplications.length > 0 && ( - <> -

    APIs

    -
    - - -
    {dataset.name}
    -
    -
    - -

    Which {dataset.name} Environment?

    - { dataset.environments != null ? ( -
    - - - - - { dataset.environments.filter(e => e.active).map(e => ( - {e.name} : {e.flow} - ))} - - - -
    - ): false} - -

    This API is Protected with the OAuth Flow, so requesting as yourself.

    -
    - - - -
    -

    OR Select which application will be using this API?

    - - - - { data.allApplications == null || data.allApplications.length == 0 ? ( - To get started, you must Register an Application first. - ) : ( - <> - - - - { data.allApplications.map(e => ( - {e.name} - ))} - - - - - ) } - - -

    All terms and conditions agreed?

    - -

    Additional Instruction

    -
    -