diff --git a/README.md b/README.md index fbbeaf8bc..cdaf84002 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The repo is setup to create a local deployment of the Portal along with required docker build -t gwa-api:e2e . ``` -1. Build: Back in `api-services-portal`, run `docker compose --profile testsuite build`. +1. Build: Back in `api-services-portal`, run `docker compose build`. 1. Run: `docker compose up`. Wait for startup to complete - look for `Swagger UI registered`. 1. The Portal is now live at http://oauth2proxy.localtest.me:4180 1. To login, use username `janis@idir` and password `awsummer` (or username `local` and password `local`). @@ -35,7 +35,12 @@ The repo is setup to create a local deployment of the Portal along with required ### Cypress testing -To run the Cypress test automation suite, run `docker compose --profile testsuite up`. +To run the Cypress test automation suite, run + +```sh +docker compose --profile testsuite build +docker compose --profile testsuite up +``` ### gwa CLI configuration diff --git a/docker-compose.yml b/docker-compose.yml index f8a230546..d75a52715 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ x-common-variables: &common-variables services: keycloak: - image: jboss/keycloak:15.1.1 + image: quay.io/keycloak/keycloak:15.1.1 container_name: keycloak hostname: keycloak depends_on: diff --git a/src/mocks/resolvers/api-directory.js b/src/mocks/resolvers/api-directory.js index 610852f4e..6d1b06346 100644 --- a/src/mocks/resolvers/api-directory.js +++ b/src/mocks/resolvers/api-directory.js @@ -1,4 +1,77 @@ +import YAML from 'js-yaml'; + +const markdown = YAML.load(` +notes: | + Here is some markdown. + # Heading 1 + ## Heading 2 + ### Heading 3 + #### Heading 4 + Then I will do **bold**, _italics_ and ~~strikethrough~~. + + #### Heading 4 + + And some text. + + How about a table? + | Col1 | Col2 | + | ---- | ---- | + | Val1 | Val2 | + + How about a new line. + + And another line. + + Try a list: + - one + - two + - three + + Or an ordered list: + 1. one + 1. two + 1. three + + Then there are images + + ![image](http://localhost:3000/images/bc_logo_header.svg) + + And links [my docs](https://github.com). + + Here are some block quotes + + > A block quote about something. + + What about a bit of code - \`alert("hi")\`. + + Code block? + \`\`\` + function (a) { + // comment + } + \`\`\` + +`); + const directories = [ + { + id: 'api1', + name: 'markdown-test', + title: 'Testing Markdown on Dataset', + notes: markdown.notes, + sector: 'Natural Resources', + license_title: 'Access Only', + view_audience: 'Named users', + security_class: 'LOW-PUBLIC', + record_publish_date: '2020-04-28', + tags: '["API","CDOGS","Document","Document Generation"]', + organization: { + title: 'Ministry of Environment and Climate Change Strategy', + }, + organizationUnit: { + title: 'Information Innovation and Technology', + }, + }, { id: 'api1', name: 'common-service-api', diff --git a/src/nextapp/pages/devportal/api-directory/[id].tsx b/src/nextapp/pages/devportal/api-directory/[id].tsx index 9a8475efb..a1a752859 100644 --- a/src/nextapp/pages/devportal/api-directory/[id].tsx +++ b/src/nextapp/pages/devportal/api-directory/[id].tsx @@ -26,6 +26,7 @@ import ReactMarkdownWithHtml from 'react-markdown/with-html'; import gfm from 'remark-gfm'; import { uid } from 'react-uid'; import { useAuth } from '@/shared/services/auth'; +import style from '@/shared/styles/markdown.module.css'; const renderers = { link: InternalLink, @@ -144,20 +145,28 @@ const ApiPage: React.FC< About This Dataset - + {data?.notes} - {data?.products?.sort((a,b) => (a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0)).map((p) => ( - - ))} + {data?.products + ?.sort((a, b) => + a.name > b.name ? 1 : b.name > a.name ? -1 : 0 + ) + .map((p) => ( + + ))} diff --git a/src/nextapp/shared/styles/markdown.module.css b/src/nextapp/shared/styles/markdown.module.css new file mode 100644 index 000000000..a6e77eb52 --- /dev/null +++ b/src/nextapp/shared/styles/markdown.module.css @@ -0,0 +1,72 @@ +.markdown { +} + +.markdown h1 { + margin-bottom: 1em; +} + +.markdown h2 { + margin-bottom: 1em; +} + +.markdown h3 { + margin-bottom: 1em; +} + +.markdown h4 { + margin-bottom: 1em; +} + +.markdown ul { + margin-top: 1em; + margin-bottom: 1em; + list-style: disc outside none; +} + +.markdown ul li { + margin-left: 2em; + display: list-item; + text-align: -webkit-match-parent; +} + +.markdown ol { + margin-top: 1em; + margin-bottom: 1em; +} + +.markdown ol li { + margin-left: 2em; + display: list-item; + text-align: -webkit-match-parent; +} + +.markdown img { + display: none; +} + +.markdown table { + margin-top: 1em; + margin-bottom: 1em; + width: 100%; + border-collapse: collapse; +} + +.markdown td { + padding: 8px; + border: 1px solid #cccccc; +} + +.markdown th { + padding: 8px; + text-align: left; + border: 1px solid #cccccc; +} + +.markdown blockquote { + margin-top: 1em; + margin-bottom: 1em; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 5px; + border-left: 10px solid #cccccc; +} diff --git a/src/package.json b/src/package.json index 5ab4e899e..d3bf15ed6 100644 --- a/src/package.json +++ b/src/package.json @@ -33,6 +33,7 @@ "copy-keystone-admin-assets": "ts-node tools/copyKeystoneAdminAssets", "x-prestart": "npm run build", "x-dev": "nodemon", + "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", diff --git a/src/services/keystone/application.ts b/src/services/keystone/application.ts index d09b507d7..ac9f7142c 100644 --- a/src/services/keystone/application.ts +++ b/src/services/keystone/application.ts @@ -12,6 +12,10 @@ export async function lookupApplication( allApplications(where: {id: $id}) { id appId + name + owner { + name + } } }`, variables: { id }, diff --git a/src/services/kong/consumer-service.ts b/src/services/kong/consumer-service.ts index 194f786ab..aa8a707ee 100644 --- a/src/services/kong/consumer-service.ts +++ b/src/services/kong/consumer-service.ts @@ -10,6 +10,8 @@ import { KeyAuthResponse, KongConsumer, } from './types'; +import { Application } from '../keystone/types'; +import { alphanumericNoSpaces } from '../utils'; const logger = Logger('kong.consumer'); @@ -41,7 +43,8 @@ export class KongConsumerService { public async createOrGetConsumer( username: string, - customId: string + customId: string, + app: Application ): Promise { logger.debug('createOrGetConsumer'); try { @@ -51,20 +54,28 @@ export class KongConsumerService { return { created: false, consumer: result }; } catch (err) { logger.debug('createOrGetConsumer - CATCH ERROR %s', err); - const result = await this.createKongConsumer(username, customId); + const result = await this.createKongConsumer(username, customId, app); logger.debug('createOrGetConsumer - CATCH RESULT %j', result); - return { created: false, consumer: result }; + return { created: true, consumer: result }; } } - public async createKongConsumer(username: string, customId: string) { + public async createKongConsumer( + username: string, + customId: string, + app: Application + ) { let body: KongConsumer = { username: username, - tags: ['aps-portal'], + tags: [], }; if (customId) { body['custom_id'] = customId; } + if (app) { + body.tags.push(`app:${alphanumericNoSpaces(app.name)}`); + body.tags.push(`owner:${alphanumericNoSpaces(app.owner.name)}`); + } logger.debug('[createKongConsumer] %s', `${this.kongUrl}/consumers`); try { let response = await fetch(`${this.kongUrl}/consumers`, { diff --git a/src/services/utils.ts b/src/services/utils.ts index ee8b8e195..f6411473a 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -75,3 +75,7 @@ export async function fetchWithTimeout(resource: string, options: any = {}) { return response; } + +export function alphanumericNoSpaces(str: string) { + return str.replace(/[^A-Za-z0-9:-]/gim, '').replace(/[:]/gim, '-'); +} diff --git a/src/services/workflow/create-service-account.ts b/src/services/workflow/create-service-account.ts index 474396e75..dc0b21846 100644 --- a/src/services/workflow/create-service-account.ts +++ b/src/services/workflow/create-service-account.ts @@ -79,7 +79,11 @@ export const CreateServiceAccount = async ( const nickname = client.client.clientId; const kongApi = new KongConsumerService(process.env.KONG_URL); - const consumer = await kongApi.createKongConsumer(nickname, clientId); + const consumer = await kongApi.createKongConsumer( + nickname, + clientId, + application + ); const consumerPK = await AddClientConsumer( context, nickname, diff --git a/src/services/workflow/generate-credential.ts b/src/services/workflow/generate-credential.ts index bbe605709..3fb2466bd 100644 --- a/src/services/workflow/generate-credential.ts +++ b/src/services/workflow/generate-credential.ts @@ -53,7 +53,12 @@ export const generateCredential = async ( const nickname = clientId; - const newApiKey = await registerApiKey(context, clientId, nickname); + const newApiKey = await registerApiKey( + context, + clientId, + nickname, + application + ); logger.debug('new-api-key CREATED FOR %s', clientId); @@ -138,7 +143,11 @@ export const generateCredential = async ( logger.debug('new-client %j', newClient); const kongApi = new KongConsumerService(process.env.KONG_URL); - const consumer = await kongApi.createKongConsumer(nickname, clientId); + const consumer = await kongApi.createKongConsumer( + nickname, + clientId, + application + ); const consumerPK = await AddClientConsumer( context, nickname, diff --git a/src/services/workflow/kong-api-key.ts b/src/services/workflow/kong-api-key.ts index 0c9285bfc..92d4d65b9 100644 --- a/src/services/workflow/kong-api-key.ts +++ b/src/services/workflow/kong-api-key.ts @@ -1,5 +1,6 @@ const { addKongConsumer } = require('../../services/keystone'); +import { Application } from '../keystone/types'; import { KongConsumerService } from '../kong'; /** @@ -14,11 +15,12 @@ import { KongConsumerService } from '../kong'; export async function registerApiKey( context: any, newClientId: string, - nickname: string + nickname: string, + app: Application ) { const kongApi = new KongConsumerService(process.env.KONG_URL); - const consumer = await kongApi.createKongConsumer(nickname, newClientId); + const consumer = await kongApi.createKongConsumer(nickname, newClientId, app); const apiKey = await kongApi.addKeyAuthToConsumer(consumer.id); diff --git a/src/services/workflow/link-consumer-to-namespace.ts b/src/services/workflow/link-consumer-to-namespace.ts index 95d604d0e..8e18e98e1 100644 --- a/src/services/workflow/link-consumer-to-namespace.ts +++ b/src/services/workflow/link-consumer-to-namespace.ts @@ -30,6 +30,7 @@ export const LinkConsumerToNamespace = async ( const kongApi = new KongConsumerService(process.env.KONG_URL); const consumerResult = await kongApi.createOrGetConsumer( consumerUsername, + null, null ); const consumerPK: any = { id: null }; diff --git a/src/test/services/utils.test.js b/src/test/services/utils.test.js new file mode 100644 index 000000000..9aa7fe164 --- /dev/null +++ b/src/test/services/utils.test.js @@ -0,0 +1,51 @@ +import { alphanumericNoSpaces } from '../../services/utils'; + +describe('alphanumericNoSpaces tests', () => { + it('should remove spaces from the string', () => { + const input = 'hello world'; + const expectedOutput = 'helloworld'; + expect(alphanumericNoSpaces(input)).toEqual(expectedOutput); + }); + + it('should remove special characters', () => { + const input = 'hello@world!how%^&*are you?'; + const expectedOutput = 'helloworldhowareyou'; + expect(alphanumericNoSpaces(input)).toEqual(expectedOutput); + }); + + it('should replace colons with hyphens', () => { + const input = 'this:is:a:test'; + const expectedOutput = 'this-is-a-test'; + expect(alphanumericNoSpaces(input)).toEqual(expectedOutput); + }); + + it('should handle empty string', () => { + const input = ''; + const expectedOutput = ''; + expect(alphanumericNoSpaces(input)).toEqual(expectedOutput); + }); + + it('should handle string with only special characters', () => { + const input = '@#$%^&*()'; + const expectedOutput = ''; + expect(alphanumericNoSpaces(input)).toEqual(expectedOutput); + }); + + it('should handle string with only colons', () => { + const input = ':::'; + const expectedOutput = '---'; + expect(alphanumericNoSpaces(input)).toEqual(expectedOutput); + }); + + it('should handle string with only alphanumeric characters', () => { + const input = 'abcdef12345'; + const expectedOutput = 'abcdef12345'; + expect(alphanumericNoSpaces(input)).toEqual(expectedOutput); + }); + + it('should handle string with mixed characters', () => { + const input = 'hello!-world, how:are?you'; + const expectedOutput = 'hello-worldhow-areyou'; + expect(alphanumericNoSpaces(input)).toEqual(expectedOutput); + }); +});