diff --git a/.env.example b/.env.example index ff9cd4a..2117445 100644 --- a/.env.example +++ b/.env.example @@ -14,4 +14,10 @@ EMAIL_SERVER_PORT= EMAIL_FROM= # comma separated list of emails allowed to sign in to the app -ALLOWED_EMAILS= \ No newline at end of file +ALLOWED_EMAILS= + +# used by the tests.js script. You must create the test credentials via the API Management page +# when spinning this application, and using localhost as the origin +TEST_VENDOR_ID= +TEST_API_KEY= +TEST_API_PORT= \ No newline at end of file diff --git a/README.md b/README.md index f7eacc8..1a3f60f 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ ```mermaid sequenceDiagram Human->>+Chiron: Accesses the service based on the ALLOWED_EMAILS list - Human->>+Chiron: Provides vendor information (via API Management page) + Human->>+Chiron: Provides vendor information via API Management page (name, origin URL, callback URL) Chiron->>+Human: Outputs vendor-specific API keys Human->>+App: Configures App to hit Chiron with given API keys - App->>+Chiron: Hits Chiron with any data generated by AI (or not) + App->>+Chiron: Hits Chiron with any data generated by AI (or not) with a POST request to /api/data/completions Human->>+Chiron: Review generated data (Human-in-the-loop) - Chiron->>+App: Hits App back with reviewed data + Chiron->>+App: Hits App back with reviewed data with a POST request to the callback URL ``` ## Development diff --git a/app/api/data/completions/review/route.js b/app/api/data/completions/review/route.js index 11e8d0e..b4ba631 100644 --- a/app/api/data/completions/review/route.js +++ b/app/api/data/completions/review/route.js @@ -35,6 +35,10 @@ export async function POST(req) { const result = await reviewCompletion(data, direction); + if (result?.acknowledged || result?.insertedId) { + // TODO: Call the vendor's webhook based on the result property + } + return new NextResponse(JSON.stringify(result), { status: 200, }); diff --git a/app/api/data/completions/route.js b/app/api/data/completions/route.js index df8b911..2686509 100644 --- a/app/api/data/completions/route.js +++ b/app/api/data/completions/route.js @@ -15,13 +15,13 @@ export async function POST(req) { ]; if (!vendorId) { - return new NextResponse("Bad Request", { + return new NextResponse(JSON.stringify("Bad Request"), { status: 400, }); } if (!apiKey) { - return new NextResponse("Bad Request", { + return new NextResponse(JSON.stringify("Bad Request"), { status: 400, }); } @@ -31,7 +31,7 @@ export async function POST(req) { const { host: vendorHost } = new URL(result.vendorUrl); if (vendorHost !== originHost) { - return new NextResponse("Forbidden", { + return new NextResponse(JSON.stringify("Forbidden"), { status: 403, }); } @@ -39,7 +39,7 @@ export async function POST(req) { const decryptedApiKey = decrypt(result.apiKey); if (decryptedApiKey !== apiKey) { - return new NextResponse("Unauthorized", { + return new NextResponse(JSON.stringify("Unauthorized"), { status: 401, }); } @@ -47,7 +47,7 @@ export async function POST(req) { const body = await req.json(); if (!body || !body?._id) { - return new NextResponse("Bad Request", { + return new NextResponse(JSON.stringify("Bad Request"), { status: 400, }); } @@ -63,13 +63,13 @@ export async function POST(req) { try { await saveCompletion(data); - return new NextResponse("Created", { + return new NextResponse(JSON.stringify("Created"), { status: 201, }); } catch (error) { console.error(error); - return new NextResponse("Error", { + return new NextResponse(JSON.stringify("Error"), { status: 500, }); } diff --git a/app/completions/approved/page.js b/app/completions/approved/page.js index 82c73d5..cf9aa70 100644 --- a/app/completions/approved/page.js +++ b/app/completions/approved/page.js @@ -9,7 +9,7 @@ export default async function Home() { if (!approvedCompletions || approvedCompletions.length === 0) { return ( - + ); } diff --git a/app/completions/pending/page.js b/app/completions/pending/page.js index ff9a43b..547073a 100644 --- a/app/completions/pending/page.js +++ b/app/completions/pending/page.js @@ -8,7 +8,7 @@ export default async function PendingCompletionsReviewPage() { const pendingReviews = await res.json(); if (!pendingReviews || pendingReviews.length === 0) { - return ; + return ; } const completions = pendingReviews.map( diff --git a/app/completions/rejected/page.js b/app/completions/rejected/page.js index 0e969ad..d9fb0e7 100644 --- a/app/completions/rejected/page.js +++ b/app/completions/rejected/page.js @@ -9,7 +9,7 @@ export default async function Home() { if (!rejectedCompletions || rejectedCompletions.length === 0) { return ( - + ); } diff --git a/bun.lockb b/bun.lockb index 014a7f8..0281459 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/containers/api-management.js b/containers/api-management.js index 1ba6ced..b93b84f 100644 --- a/containers/api-management.js +++ b/containers/api-management.js @@ -9,6 +9,7 @@ import { Heading, List, Menu, + Text, TextInput, } from "grommet"; import { useState } from "react"; @@ -89,6 +90,17 @@ export default function ApiManagementContainer(props) {
( + + {item.name} + + )} + secondaryKey={(item) => ( + + Callback URL: {item.callbackUrl} + + )} + itemKey={(item) => item.name} pad={{ left: "small", right: "none" }} action={(item, index) => ( apiKey.vendorName); + const vendors = result.map((apiKey) => ({ + name: apiKey.vendorName, + callbackUrl: apiKey.vendorCallbackUrl, + })); return vendors; } diff --git a/package.json b/package.json index 609bb62..24762a4 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "start": "next start", "lint": "next lint", "semantic-release": "semantic-release", - "test": "sh test.sh" + "test": "node -r dotenv/config tests.js" }, "dependencies": { "@auth/mongodb-adapter": "^2.0.1", @@ -25,6 +25,7 @@ "styled-components": "5" }, "devDependencies": { + "dotenv": "^16.3.1", "eslint": "^8", "eslint-config-next": "13.5.4", "eslint-config-prettier": "^9.0.0", diff --git a/test.sh b/test.sh deleted file mode 100644 index fc0099d..0000000 --- a/test.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash - -uuid=$(uuidgen) -random_str=$(shuf -er -n8 {A..Z} {a..z} {0..9} | paste -sd "") - - -# Request 1 - with valid vendorId and apiKey headers -response1=$(curl -X POST \ - http://localhost:3000/api/data/completions \ - -H 'vendorId: 572FCEFE637B8AC7' \ - -H 'apiKey: 25kcnWwj1G51wavfyK0vJyvx4sWisksypyI4kV7P3m/3W1oEpPEjizXDHm2bWFIM0u2Ir42mx/TZXd4ioQBTqA==' \ - -d '{ - "_id": "'$uuid'", - "property1": "'$random_str'", - "property2": "'$random_str'" -}') - -if [[ $response1 == *"Bad Request"* ]]; then - echo "Request 1 failed with error: Bad Request" -else - echo "Request 1 succeeded with response:" - echo $response1 -fi - -# New line -echo - -# # Request 2 - with invalid vendorId and apiKey headers -# response2=$(curl -X POST \ -# http://localhost:3000/api/data/completions \ -# -H 'vendorId: invalid' \ -# -H 'apiKey: invalid' \ -# -d '{ -# "property1": "value1", -# "property2": "value2" -# }') - -# if [[ $response2 == *"Bad Request"* ]]; then -# echo "Request 2 failed with error: Bad Request" -# else -# echo "Request 2 succeeded with response:" -# echo $response2 -# fi - -# # New line -# echo - -# # Request 3 - with vendorId header only -# response3=$(curl -X POST \ -# http://localhost:3000/api/data/completions \ -# -H 'vendorId: your_vendor_id' \ -# -d '{ -# "property1": "value1", -# "property2": "value2" -# }') - -# if [[ $response3 == *"Bad Request"* ]]; then -# echo "Request 3 failed with error: Bad Request" -# else -# echo "Request 3 succeeded with response:" -# echo $response3 -# fi - -# # New line -# echo - -# # Request 4 - with apiKey header only -# response4=$(curl -X POST \ -# http://localhost:3000/api/data/completions \ -# -H 'apiKey: your_api_key' \ -# -d '{ -# "property1": "value1", -# "property2": "value2" -# }') - -# if [[ $response4 == *"Bad Request"* ]]; then -# echo "Request 4 failed with error: Bad Request" -# else -# echo "Request 4 succeeded with response:" -# echo $response4 -# fi - -# # New line -# echo - -# # Request 5 - no headers -# response5=$(curl -X POST \ -# http://localhost:3000/api/data/completions \ -# -d '{ -# "property1": "value1", -# "property2": "value2" -# }') - -# if [[ $response5 == *"Bad Request"* ]]; then -# echo "Request 5 failed with error: Bad Request" -# else -# echo "Request 5 succeeded with response:" -# echo $response5 -# fi \ No newline at end of file diff --git a/tests.js b/tests.js new file mode 100644 index 0000000..2a45d64 --- /dev/null +++ b/tests.js @@ -0,0 +1,69 @@ +const http = require("http"); +const crypto = require("crypto"); + +const vendorId = process.env.TEST_VENDOR_ID; +const apiKey = process.env.TEST_API_KEY; +const testApiPort = process.env.TEST_API_PORT; + +function generateUUID() { + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => + (c ^ (crypto.randomBytes(1)[0] & (15 >> (c / 4)))).toString(16), + ); +} + +function generateRandomString() { + return crypto.randomBytes(4).toString("hex"); +} + +// Create a server that listens to requests on port 3001 +const server = http.createServer((req, res) => { + // Set CORS headers + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Request-Method", "*"); + res.setHeader("Access-Control-Allow-Methods", "OPTIONS, POST"); + res.setHeader("Access-Control-Allow-Headers", "*"); + + if (req.method === "OPTIONS") { + res.writeHead(200); + res.end(); + return; + } + + // Handle POST requests + if (req.method === "POST") { + let data = ""; + + req.on("data", (chunk) => { + data += chunk; + }); + + req.on("end", () => { + console.log(JSON.parse(data)); + res.writeHead(200); + res.end(); + }); + } +}); + +server.listen(testApiPort, () => { + console.log(`Test API server listening on port ${testApiPort}`); + + fetch("http://localhost:3000/api/data/completions", { + method: "POST", + headers: { + host: `localhost:${testApiPort}`, // this is hidden in real world scenarios + vendorId, + apiKey, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + _id: generateUUID(), + property1: generateRandomString(), + property2: generateRandomString(), + }), + }) + .then((res) => res.json()) + .then((json) => { + console.log(json); + }); +});