diff --git a/bruno/APIM/.gitignore b/bruno/APIM/.gitignore new file mode 100644 index 0000000..18aeac8 --- /dev/null +++ b/bruno/APIM/.gitignore @@ -0,0 +1,3 @@ + +**/node_modules/** + diff --git a/bruno/APIM/Get_Auth_Token.bru b/bruno/APIM/Get_Auth_Token.bru new file mode 100644 index 0000000..386760c --- /dev/null +++ b/bruno/APIM/Get_Auth_Token.bru @@ -0,0 +1,72 @@ +meta { + name: Get Auth Token + type: http + seq: 1 +} + +post { + url: https://internal-dev.api.service.nhs.uk/oauth2/token + body: formUrlEncoded + auth: none +} + +body:form-urlencoded { + grant_type: client_credentials + client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-bearer +} + +script:pre-request { + const jwt = require("jsonwebtoken"); + const fs = require("fs"); + const crypto = require("crypto"); + + // Read env vars + const secret = bru.getEnvVar("JWT_SECRET"); + const privateKeyPath = bru.getEnvVar("PRIVATE_KEY_PATH"); + + // Enviroment variables check + if (!secret) { + throw new Error("JWT_SECRET environment variable is missing."); + } + if (!privateKeyPath) { + throw new Error("PRIVATE_KEY_PATH environment variable is missing."); + } + + const privateKey = fs.readFileSync(privateKeyPath) + + const payload = { + sub: secret, + iss: secret, + jti: crypto.randomUUID(), + aud: "https://internal-dev.api.service.nhs.uk/oauth2/token", + exp: (Date.now()/1000) + 300 + }; + + const options = { + algorithm:'RS512', + header: {kid: "kid-1"} + } + + + // Sign token + const token = jwt.sign(payload, privateKey, options); + + + let new_body = req.getBody(); + new_body.push({name: "client_assertion", value:token}) + + req.setBody(new_body) + + + // For debugging: see the generated token in the console + // console.log("Generated JWT:", token); +} + +script:post-response { + bru.setEnvVar("auth_token", res.getBody().access_token) +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/APIM/Get_Healthcheck_status.bru b/bruno/APIM/Get_Healthcheck_status.bru new file mode 100644 index 0000000..09c9c70 --- /dev/null +++ b/bruno/APIM/Get_Healthcheck_status.bru @@ -0,0 +1,15 @@ +meta { + name: Get Healthcheck status + type: http + seq: 3 +} + +get { + url: https://internal-dev.api.service.nhs.uk/pathology-laboratory-reporting/_status + body: none + auth: inherit +} + +settings { + encodeUrl: true +} diff --git a/bruno/APIM/Post_Document_Bundle_via_APIM.bru b/bruno/APIM/Post_Document_Bundle_via_APIM.bru new file mode 100644 index 0000000..73ebe35 --- /dev/null +++ b/bruno/APIM/Post_Document_Bundle_via_APIM.bru @@ -0,0 +1,39 @@ +meta { + name: Post Document Bundle via APIM + type: http + seq: 2 +} + +post { + url: https://internal-dev.api.service.nhs.uk/pathology-laboratory-reporting/FHIR/R4/Bundle + body: json + auth: inherit +} + +headers { + Content-Type: application/fhir+json +} + +body:json { + { + "resourceType": "Bundle", + "type": "document", + "entry": [ + { + "fullUrl": "patient", + "resource": { + "resourceType": "Patient", + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "test-nhs-number" + } + } + } + ] + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/APIM/bruno.json b/bruno/APIM/bruno.json new file mode 100644 index 0000000..4e865f6 --- /dev/null +++ b/bruno/APIM/bruno.json @@ -0,0 +1,19 @@ +{ + "version": "1", + "name": "APIM", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ], + "scripts": { + "moduleWhiteList": [ + "jsonwebtoken", + "fs", + "crypto" + ], + "filesystemAccess": { + "allow": true + } + } +} diff --git a/bruno/APIM/collection.bru b/bruno/APIM/collection.bru new file mode 100644 index 0000000..3458806 --- /dev/null +++ b/bruno/APIM/collection.bru @@ -0,0 +1,7 @@ +auth { + mode: bearer +} + +auth:bearer { + token: {{auth_token}} +} diff --git a/bruno/APIM/environments/APIM.bru b/bruno/APIM/environments/APIM.bru new file mode 100644 index 0000000..9048c2a --- /dev/null +++ b/bruno/APIM/environments/APIM.bru @@ -0,0 +1,5 @@ +vars:secret [ + PRIVATE_KEY_PATH, + JWT_SECRET, + auth_token +] diff --git a/bruno/APIM/package-lock.json b/bruno/APIM/package-lock.json new file mode 100644 index 0000000..029b5ea --- /dev/null +++ b/bruno/APIM/package-lock.json @@ -0,0 +1,154 @@ +{ + "name": "apim", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "apim", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "jsonwebtoken": "^9.0.3" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + } + } +} diff --git a/bruno/APIM/package.json b/bruno/APIM/package.json new file mode 100644 index 0000000..a2f413c --- /dev/null +++ b/bruno/APIM/package.json @@ -0,0 +1,16 @@ +{ + "name": "apim", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "jsonwebtoken": "^9.0.3" + } +} diff --git a/pathology-api/openapi.yaml b/pathology-api/openapi.yaml index 3ff0a1e..e5eae9b 100644 --- a/pathology-api/openapi.yaml +++ b/pathology-api/openapi.yaml @@ -1,291 +1,175 @@ openapi: 3.0.3 info: - title: pathology API - description: Clinical Data pathology API + title: Pathology and Laboratory Medicine Reporting - FHIR API + description: Clinical Data Pathology API version: 0.1.0 contact: name: API Support servers: - url: http://localhost:5001 description: Local development server +components: + securitySchemes: + app-level3: + $ref: https://proxygen.ptl.api.platform.nhs.uk/components/securitySchemes/app-level3 paths: - /2015-03-31/functions/function/invocations: + /FHIR/R4/Bundle: post: - summary: Get hello world message - description: Returns a simple hello world message - operationId: postHelloWorld + security: + - app-level3: [] + summary: Provide a new test result + description: Provide a new test result to the pathology API + operationId: postBundle requestBody: - required: false + required: true content: - application/json: + application/fhir+json: schema: type: object + required: + - resourceType + - type + - entry properties: - payload: + resourceType: type: string - description: The payload to be processed - responses: - '200': - description: Successful response - content: - text/plain: - schema: - type: object - properties: - status_code: - type: integer - description: Status code of the interaction - body: - type: string - description: The output of the interaction - errorMessage: - type: string - description: Any error messages relating to errors encountered with the interaction - errorType: - type: string - description: The type of error encountered during the interaction, if an error has occurred - requestId: - type: string - format: uuid - description: The unique request ID for the interaction - stacktrace: - type: array - items: - type: string - description: The stack trace of the error, if an error has occurred - get: - summary: Get hello world message - description: Returns a simple hello world message - operationId: postHelloWorld - requestBody: - required: false - content: - application/json: - schema: - type: object - properties: - payload: + enum: + - Bundle + description: FHIR resource type + type: type: string - description: The payload to be processed + enum: + - document + - transaction + description: The type of the bundle + example: document + entry: + type: array + description: Entries in the bundle + minItems: 1 + maxItems: 1 + items: + type: object + required: + - fullUrl + - resource + properties: + fullUrl: + type: string + description: URI for resource + example: "patient" + resource: + type: object + required: + - resourceType + - identifier + properties: + resourceType: + type: string + description: Type of FHIR resource (Always "Patient") + enum: + - Patient + example: Patient + identifier: + type: object + required: + - system + - value + properties: + system: + type: string + enum: + - "https://fhir.nhs.uk/Id/nhs-number" + example: "https://fhir.nhs.uk/Id/nhs-number" + value: + type: string + example: "9999999999" responses: '200': description: Successful response content: - text/plain: + application/fhir+json: schema: - type: object - properties: - status_code: - type: integer - description: Status code of the interaction - body: - type: string - description: The output of the interaction - errorMessage: - type: string - description: Any error messages relating to errors encountered with the interaction - errorType: - type: string - description: The type of error encountered during the interaction, if an error has occurred - requestId: - type: string - format: uuid - description: The unique request ID for the interaction - stacktrace: - type: array - items: - type: string - description: The stack trace of the error, if an error has occurred - - '404': - description: Route not found - content: - text/html: - schema: - type: string - put: - summary: Get hello world message - description: Returns a simple hello world message - operationId: postHelloWorld - requestBody: - required: false - content: - application/json: - schema: + schema: type: object + required: + - resourceType + - type properties: - payload: + resourceType: type: string - description: The payload to be processed - responses: - '200': - description: Successful response - content: - text/plain: - schema: - type: object - properties: - status_code: - type: integer - description: Status code of the interaction - body: - type: string - description: The output of the interaction - errorMessage: - type: string - description: Any error messages relating to errors encountered with the interaction - errorType: - type: string - description: The type of error encountered during the interaction, if an error has occurred - requestId: - type: string - format: uuid - description: The unique request ID for the interaction - stacktrace: - type: array - items: - type: string - description: The stack trace of the error, if an error has occurred - - '404': - description: Route not found - content: - text/html: - schema: - type: string - patch: - summary: Get hello world message - description: Returns a simple hello world message - operationId: postHelloWorld - requestBody: - required: false - content: - application/json: - schema: - type: object - properties: - payload: + enum: + - Bundle + description: FHIR resource type (always "Bundle") + meta: + type: object + description: Metadata about the resource + nullable: true + type: type: string - description: The payload to be processed - responses: - '200': - description: Successful response - content: - text/plain: - schema: - type: object - properties: - status_code: - type: integer - description: Status code of the interaction - body: - type: string - description: The output of the interaction - errorMessage: - type: string - description: Any error messages relating to errors encountered with the interaction - errorType: - type: string - description: The type of error encountered during the interaction, if an error has occurred - requestId: - type: string - format: uuid - description: The unique request ID for the interaction - stacktrace: - type: array - items: + enum: + - document + - transaction + description: The type of the bundle + identifier: + type: object + nullable: true + description: Persistent identifier for the bundle (UUID) + properties: + system: type: string - description: The stack trace of the error, if an error has occurred - - '404': - description: Route not found - content: - text/html: - schema: - type: string - delete: - summary: Get hello world message - description: Returns a simple hello world message - operationId: postHelloWorld - requestBody: - required: false - content: - application/json: - schema: - type: object - properties: - payload: - type: string - description: The payload to be processed - responses: - '200': - description: Successful response + format: uri + value: + type: string + format: uuid + entry: + type: array + nullable: true + description: Entries in the bundle + items: + type: object + required: + - fullUrl + - resource + properties: + fullUrl: + type: string + description: URI for resource + resource: + type: object + required: + - resourceType + description: The Patient a test result is for + properties: + resourceType: + type: string + description: Type of FHIR resource (always "Patient") + enum: + - Patient + example: Patient + '400': + description: Invalid request content: text/plain: schema: - type: object - properties: - status_code: - type: integer - description: Status code of the interaction - body: - type: string - description: The output of the interaction - errorMessage: - type: string - description: Any error messages relating to errors encountered with the interaction - errorType: - type: string - description: The type of error encountered during the interaction, if an error has occurred - requestId: - type: string - format: uuid - description: The unique request ID for the interaction - stacktrace: - type: array - items: - type: string - description: The stack trace of the error, if an error has occurred - trace: - summary: Get hello world message - description: Returns a simple hello world message - operationId: postHelloWorld - requestBody: - required: false - content: - application/json: - schema: - type: object - properties: - payload: - type: string - description: The payload to be processed - responses: - '200': - description: Successful response + type: string + '404': + description: Content not found content: text/plain: schema: - type: object - properties: - status_code: - type: integer - description: Status code of the interaction - body: - type: string - description: The output of the interaction - errorMessage: - type: string - description: Any error messages relating to errors encountered with the interaction - errorType: - type: string - description: The type of error encountered during the interaction, if an error has occurred - requestId: - type: string - format: uuid - description: The unique request ID for the interaction - stacktrace: - type: array - items: - type: string - description: The stack trace of the error, if an error has occurred + type: string + +x-nhsd-apim: + monitoring: false + access: + - title: Application Restricted + grants: + app-level3: [] + target: + type: external + healthcheck: /_status + url: https://%url-section%.endpoints.pathology-laboratory-reporting.national.nhs.uk + security: + type: mtls + secret: ~~mtls_secret_name~~