diff --git a/.eslintrc.js b/.eslintrc.js
index 35caa072..bcd53720 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -42,5 +42,6 @@ module.exports = {
rules: {
"react/react-in-jsx-scope": "off", //React is provided by NextJS instead
"react/prop-types": "off", //strict TypeScript provides the same benefits
+ "camelcase": ["error", {"allow": ["user_key"]}], // parameter named by external API gateway and used in index.tsx
}
}
diff --git a/README.md b/README.md
index 6ee250b2..6dd496d7 100644
--- a/README.md
+++ b/README.md
@@ -6,19 +6,30 @@ relates to weekly certification
## Running the Application
**Prerequisites:**
- - Node 12+
- - yarn
+
+- Node 12+
+- yarn
**Run this app**
-Clone this repo, and then run:
+1. Clone this repo
+2. Run `yarn install`
+3. Define environment variables (see below)
+4. Run `yarn dev`
+5. Open [http://localhost:3000/claimstatus](http://localhost:3000/claimstatus) with your browser to see the result
-```bash
-yarn install
-yarn dev
-```
+### Environment Variables
+
+- ID_HEADER_NAME: The name of the header that contains the EDD-defined unique ID / "Unique Number" in the incoming request
+- API_URL: The url for the API
+- API_USER_KEY: The user key for the API
+- CERTIFICATE_DIR: The path to the client certificate (certificate must be in PFX/P12 format)
+- P12_FILE: The name of the client certificate file
+
+For local development:
-Open [http://localhost:3000/claimstatus](http://localhost:3000/claimstatus) with your browser to see the result.
+1. Create a `.env` file in the root of this repo
+2. Define each of the environment variables above
## Running the test suite
diff --git a/package.json b/package.json
index c5bc251f..ac518fa9 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,10 @@
"dependencies": {
"@types/pino": "^6.3.8",
"bootstrap": "^4.6.0",
+ "dotenv": "^9.0.2",
"next": "10.0.8",
+ "node-fetch": "^2.6.1",
+ "pem": "^1.14.4",
"pino": "^6.11.3",
"react": "17.0.1",
"react-bootstrap": "^1.5.2",
@@ -51,6 +54,7 @@
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^11.2.5",
"@types/node": "^14.14.33",
+ "@types/pem": "^1.9.5",
"@types/react": "^17.0.3",
"@types/react-test-renderer": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^4.19.0",
diff --git a/pages/_app.tsx b/pages/_app.tsx
index d2597be5..aaa019da 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -3,6 +3,14 @@ import { AppProps } from 'next/app'
import { ReactElement } from 'react'
import { appWithTranslation } from 'next-i18next'
+import { config } from 'dotenv'
+import { resolve } from 'path'
+
+// Loads .env for local development.
+if (process.env.NODE_ENV === 'development') {
+ config({ path: resolve(process.cwd(), '..', '.env') })
+}
+
function MyApp({ Component, pageProps }: AppProps): ReactElement {
return
}
diff --git a/pages/index.tsx b/pages/index.tsx
index 7d1398af..66c97b05 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -1,3 +1,8 @@
+import fs from 'fs'
+import path from 'path'
+import https from 'https'
+import { promisify } from 'util'
+
import Head from 'next/head'
import Container from 'react-bootstrap/Container'
import pino from 'pino'
@@ -5,12 +10,22 @@ import { ReactElement } from 'react'
import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { GetServerSideProps } from 'next'
+import pem, { Pkcs12ReadResult } from 'pem'
+import fetch, { Response } from 'node-fetch'
import { Header } from '../components/Header'
import { Main } from '../components/Main'
import { Footer } from '../components/Footer'
-export default function Home(): ReactElement {
+export interface Claim {
+ ClaimType: string | 'not working'
+}
+
+export interface HomeProps {
+ claimData?: Claim[]
+}
+
+export default function Home({ claimData }: HomeProps): ReactElement {
const { t } = useTranslation('common')
return (
@@ -19,21 +34,128 @@ export default function Home(): ReactElement {
{t('title')}
-
+ {console.dir({ claimData })} {/* @TODO: Remove. For development purposes only. */}
)
}
+export interface QueryParams {
+ user_key: string
+ uniqueNumber: string
+}
+
+function buildApiUrl(url: string, queryParams: QueryParams) {
+ const apiUrl = new URL(url)
+
+ for (const key in queryParams) {
+ apiUrl.searchParams.append(key, queryParams[key as 'user_key' | 'uniqueNumber'])
+ }
+
+ return apiUrl.toString()
+}
+
export const getServerSideProps: GetServerSideProps = async ({ req, locale }) => {
const isProd = process.env.NODE_ENV === 'production'
const logger = isProd ? pino({}) : pino({ prettyPrint: true })
logger.info(req)
+ /*
+ * Load environment variables to be used for authentication & API calls.
+ * @TODO: Handle error case where env vars are null or undefined.
+ */
+ // Request fields
+ const ID_HEADER_NAME: string = process.env.ID_HEADER_NAME ?? ''
+
+ // API fields
+ const API_URL: string = process.env.API_URL ?? ''
+ const API_USER_KEY: string = process.env.API_USER_KEY ?? ''
+
+ // TLS Certificate fields
+ const CERT_DIR: string = process.env.CERTIFICATE_DIR ?? ''
+ const P12_FILE: string = process.env.P12_FILE ?? ''
+ const P12_PATH: string = path.join(CERT_DIR, P12_FILE)
+
+ let apiData: JSON | null = null
+
+ // Returns certificate object with cert, key, and ca fields.
+ // https://dexus.github.io/pem/jsdoc/module-pem.html#.readPkcs12
+ async function getCertificate() {
+ const pemReadPkcs12 = promisify(pem.readPkcs12)
+ const pfx = fs.readFileSync(P12_PATH)
+
+ // TS does not play very nicely with util.promisify
+ // See, e.g., https://github.com/Microsoft/TypeScript/issues/26048
+ // Non-MVP TODO: Consider removing this ignore & TypeScriptifying.
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore -- TypeScript does not handle promisify well.
+ const keybundle = await pemReadPkcs12(pfx)
+ return keybundle
+ }
+
+ // Takes certificate that getCertificate function returns as argument,
+ // makes API call, returns all API data.
+ async function makeRequest(certificate: Pkcs12ReadResult) {
+ const headers = {
+ Accept: 'application/json',
+ }
+
+ // https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options
+ const options = {
+ cert: certificate.cert,
+ key: certificate.key,
+ rejectUnauthorized: false,
+ keepAlive: false,
+ }
+
+ // Instantiate agent to use with TLS Certificate.
+ // Reference: https://sebtrif.xyz/blog/2019-10-03-client-side-ssl-in-node-js-with-fetch/
+ const sslConfiguredAgent: https.Agent = new https.Agent(options)
+
+ const apiUrlParams: QueryParams = {
+ user_key: API_USER_KEY,
+ uniqueNumber: req.headers[ID_HEADER_NAME] as string,
+ }
+
+ const apiUrl: RequestInfo = buildApiUrl(API_URL, apiUrlParams)
+
+ try {
+ const response: Response = await fetch(apiUrl, {
+ headers: headers,
+ agent: sslConfiguredAgent,
+ })
+
+ // TODO: Why does @ts-ignore not work on this line?
+ // TODO: Implement proper typing of responseBody if possible.
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const responseBody: JSON = await response.json()
+
+ apiData = responseBody
+ } catch (error) {
+ console.log(error)
+ }
+
+ // Explicitly destroy agent so connection does not persist.
+ // https://nodejs.org/api/http.html#http_agent_destroy
+ // There were reports (SNAT?) of connection pool problems,
+ // which could be caused by testing? Either way, explicitly destroy the HTTP Agent.
+ sslConfiguredAgent.destroy()
+
+ return apiData
+ }
+
+ // The 3 steps where the above code is invoked and getServerSideProps returns props.
+ // Step 1: Get the certificate.
+ const certificate = await getCertificate()
+ // Step 2: Use certificate to make the API request and return the data.
+ const data = await makeRequest(certificate)
+
+ // Step 3: Return Props
return {
props: {
+ claimData: [data],
...(await serverSideTranslations(locale || 'en', ['common', 'header', 'footer'])),
},
}
diff --git a/tests/pages/__snapshots__/index.test.tsx.snap b/tests/pages/__snapshots__/index.test.tsx.snap
index 7e030a54..d9bbde3f 100644
--- a/tests/pages/__snapshots__/index.test.tsx.snap
+++ b/tests/pages/__snapshots__/index.test.tsx.snap
@@ -492,5 +492,6 @@ exports[`Exemplar react-test-renderer Snapshot test renders homepage unchanged 1
+
`;
diff --git a/yarn.lock b/yarn.lock
index c786b53a..b9eebeb3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3271,6 +3271,13 @@
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==
+"@types/pem@^1.9.5":
+ version "1.9.5"
+ resolved "https://registry.yarnpkg.com/@types/pem/-/pem-1.9.5.tgz#cd5548b5e0acb4b41a9e21067e9fcd8c57089c99"
+ integrity sha512-C0txxEw8B7DCoD85Ko7SEvzUogNd5VDJ5/YBG8XUcacsOGqxr5Oo4g3OUAfdEDUbhXanwUoVh/ZkMFw77FGPQQ==
+ dependencies:
+ "@types/node" "*"
+
"@types/pino-pretty@*":
version "4.7.0"
resolved "https://registry.yarnpkg.com/@types/pino-pretty/-/pino-pretty-4.7.0.tgz#e4a18541f8464d1cc48216f5593cc6a0e62dc2c3"
@@ -4979,6 +4986,11 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
+charenc@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
+ integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
+
chokidar@3.5.1, "chokidar@>=2.0.0 <4.0.0", chokidar@^3.4.1, chokidar@^3.4.2:
version "3.5.1"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a"
@@ -5541,6 +5553,11 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
shebang-command "^1.2.0"
which "^1.2.9"
+crypt@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
+ integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=
+
crypto-browserify@3.12.0, crypto-browserify@^3.11.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
@@ -6068,6 +6085,11 @@ dotenv@^8.0.0, dotenv@^8.2.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
+dotenv@^9.0.2:
+ version "9.0.2"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05"
+ integrity sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==
+
downshift@^6.0.15:
version "6.1.2"
resolved "https://registry.yarnpkg.com/downshift/-/downshift-6.1.2.tgz#99d9a03d4da4bf369df766effc3b70f7e789950e"
@@ -6321,6 +6343,11 @@ es5-shim@^4.5.13:
resolved "https://registry.yarnpkg.com/es5-shim/-/es5-shim-4.5.15.tgz#6a26869b261854a3b045273f5583c52d390217fe"
integrity sha512-FYpuxEjMeDvU4rulKqFdukQyZSTpzhg4ScQHrAosrlVpR6GFyaw14f74yn2+4BugniIS0Frpg7TvwZocU4ZMTw==
+es6-promisify@^6.0.0:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.1.1.tgz#46837651b7b06bf6fff893d03f29393668d01621"
+ integrity sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg==
+
es6-shim@^0.35.5:
version "0.35.6"
resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.6.tgz#d10578301a83af2de58b9eadb7c2c9945f7388a0"
@@ -8244,7 +8271,7 @@ is-boolean-object@^1.1.0:
dependencies:
call-bind "^1.0.0"
-is-buffer@^1.1.5:
+is-buffer@^1.1.5, is-buffer@~1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
@@ -9780,6 +9807,15 @@ md5.js@^1.3.4:
inherits "^2.0.1"
safe-buffer "^5.1.2"
+md5@^2.2.1:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f"
+ integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==
+ dependencies:
+ charenc "0.0.2"
+ crypt "0.0.2"
+ is-buffer "~1.1.6"
+
mdast-squeeze-paragraphs@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz#7c4c114679c3bee27ef10b58e2e015be79f1ef97"
@@ -10608,7 +10644,7 @@ os-browserify@^0.3.0:
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
-os-tmpdir@~1.0.2:
+os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
@@ -10921,6 +10957,16 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
+pem@^1.14.4:
+ version "1.14.4"
+ resolved "https://registry.yarnpkg.com/pem/-/pem-1.14.4.tgz#a68c70c6e751ccc5b3b5bcd7af78b0aec1177ff9"
+ integrity sha512-v8lH3NpirgiEmbOqhx0vwQTxwi0ExsiWBGYh0jYNq7K6mQuO4gI6UEFlr6fLAdv9TPXRt6GqiwE37puQdIDS8g==
+ dependencies:
+ es6-promisify "^6.0.0"
+ md5 "^2.2.1"
+ os-tmpdir "^1.0.1"
+ which "^2.0.2"
+
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"