Skip to content

Commit

Permalink
feat: add api management routes and screen
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelovicentegc committed Oct 14, 2023
1 parent c477066 commit e43fcf3
Show file tree
Hide file tree
Showing 17 changed files with 382 additions and 45 deletions.
15 changes: 15 additions & 0 deletions app/api-management/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Empty from "@/components/empty";

export default async function ApiManagementPage() {
const integrations = [];

if (!integrations || integrations.length === 0) {
return (
<Empty
empty={{ description: "No integrations available to manage yet" }}
/>
);
}

return integrations.map((integration) => integration._id);
}
58 changes: 58 additions & 0 deletions app/api/api-keys/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { getServerSession } from "next-auth";
import { NextResponse } from "next/server";
import { authOptions } from "../auth/[...nextauth]/route";
import { saveApiKeys } from "@/lib/db/writes";
import { encrypt } from "@/lib/encryption";

export async function POST(req, res) {
const session = await getServerSession(req, res, authOptions);

if (!session) {
return new NextResponse("Unauthorized", { status: 401 });
}

try {
const { vendorName, vendorUrl } = await req.body;

if (!vendorUrl) {
throw new Error("You must enter a vendor URL.");
}

if (!vendorName) {
throw new Error("You must enter a vendor name.");
}

// Validate URL
new URL(vendorUrl);

const vendorId = crypto.randomBytes(8).toString("hex").toUpperCase();

// Generate the API key by hashing the vendor name and URL together
const apiKey = crypto
.createHash("sha512")
.update(vendorName + "-" + vendorUrl)
.digest()
.subarray(0, 64)
.toString("base64");

const data = {
vendorId,
apiKey: encrypt(apiKey),
vendorName,
vendorUrl,
createdBy: session.user.email,
};

await saveApiKeys(data);

return new NextResponse("Created", {
status: 201,
});
} catch (error) {
console.error(error);

return new NextResponse("Error", {
status: 500,
});
}
}
9 changes: 8 additions & 1 deletion app/api/data/completions/approved/route.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { getApprovedCompletions } from "@/lib/db/reads";
import { getServerSession } from "next-auth";
import { NextResponse } from "next/server";

export async function GET(request, _) {
export async function GET() {
const session = await getServerSession(authOptions);

if (!session) {
return new NextResponse(JSON.stringify([]), { status: 401 });
}
const approvedCompletions = await getApprovedCompletions();

return new NextResponse(JSON.stringify(approvedCompletions), {
Expand Down
18 changes: 9 additions & 9 deletions app/api/data/completions/pending/route.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { getCompletionsPendingReview } from "@/lib/db/reads";
import { getServerSession } from "next-auth/next";
import { NextResponse } from "next/server";

export async function GET(request, _) {
const completionsPendingReview = await getCompletionsPendingReview();
export async function GET() {
const session = await getServerSession(authOptions);

return new NextResponse(JSON.stringify(completionsPendingReview), {
status: 200,
});
}
if (!session) {
return new NextResponse(JSON.stringify([]), { status: 401 });
}

export async function PATCH(request) {
// TODO: Approved/reject review
const completionsPendingReview = await getCompletionsPendingReview();

return new NextResponse(JSON.stringify({ answer: "John Doe" }), {
return new NextResponse(JSON.stringify(completionsPendingReview), {
status: 200,
});
}
9 changes: 8 additions & 1 deletion app/api/data/completions/rejected/route.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { getRejectedCompletions } from "@/lib/db/reads";
import { getServerSession } from "next-auth";
import { NextResponse } from "next/server";

export async function GET(request, _) {
export async function GET() {
const session = await getServerSession(authOptions);

if (!session) {
return new NextResponse(JSON.stringify([]), { status: 401 });
}
const rejectedCompletions = await getRejectedCompletions();

return new NextResponse(JSON.stringify(rejectedCompletions), {
Expand Down
45 changes: 45 additions & 0 deletions app/api/data/completions/review/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { reviewCompletion } from "@/lib/db/writes";
import { getServerSession } from "next-auth";
import { NextResponse } from "next/server";

function validateDirection(direction) {
const validDirections = [
"pending2approve",
"pending2reject",
"approve2reject",
"reject2approve",
];

if (!validDirections.includes(direction)) {
throw new Error(`Invalid direction: ${direction}`);
}
}

export async function POST(req) {
const session = await getServerSession(authOptions);

if (!session) {
return new NextResponse(JSON.stringify("Unauthorized"), { status: 401 });
}

if (!req.body._id && !req.body.direction) {
return new NextResponse(JSON.stringify("Bad Request"), { status: 400 });
}

try {
const { _id, direction } = req.body;
validateDirection(direction);

const result = await reviewCompletion(_id, direction);

return new NextResponse(JSON.stringify(result), {
status: 200,
});
} catch (error) {
console.error(error);
return new NextResponse(JSON.stringify("Internal Server Error"), {
status: 500,
});
}
}
59 changes: 59 additions & 0 deletions app/api/data/completions/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { getApiKey } from "@/lib/db/reads";
import { saveCompletion } from "@/lib/db/writes";
import { decrypt } from "@/lib/encryption";
import { NextResponse } from "next/server";

export async function POST(req) {
const { vendorId, apiKey } = req.headers;

if (!vendorId) {
return new NextResponse("Bad Request", {
status: 400,
});
}

if (!apiKey) {
return new NextResponse("Bad Request", {
status: 400,
});
}

const result = await getApiKey(vendorId);

const decryptedApiKey = decrypt(result.apiKey);

if (decryptedApiKey !== apiKey) {
return new NextResponse("Unauthorized", {
status: 401,
});
}

const { origin } = new URL(result.vendorUrl);
const { origin: requestOrigin } = new URL(req.headers.referer);

if (origin !== requestOrigin) {
return new NextResponse("Forbidden", {
status: 403,
});
}

if (!req.body) {
return new NextResponse("Bad Request", {
status: 400,
});
}

try {
await saveCompletion(req.body);

return new NextResponse("Created", {
status: 201,
});
} catch (error) {
console.error(error);

return new NextResponse("Error", {
status: 500,
});
}
}
9 changes: 0 additions & 9 deletions app/completions/approved/page.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../api/auth/[...nextauth]/route";
import { redirect } from "next/navigation";
import Empty from "@/components/empty";

export default async function Home() {
const session = await getServerSession(authOptions);

if (!session) {
return redirect("/api/auth/signin");
}

const res = await fetch(
process.env.NEXTAUTH_URL + "/api/data/completions/approved",
);
Expand Down
10 changes: 1 addition & 9 deletions app/completions/pending/page.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../api/auth/[...nextauth]/route";
import { redirect } from "next/navigation";
import Empty from "@/components/empty";

export default async function Home() {
const session = await getServerSession(authOptions);

if (!session) {
return redirect("/api/auth/signin");
}

const res = await fetch(
process.env.NEXTAUTH_URL + "/api/data/completions/pending",
);

const pendingReviews = await res.json();

if (!pendingReviews || pendingReviews.length === 0) {
Expand Down
9 changes: 0 additions & 9 deletions app/completions/rejected/page.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../api/auth/[...nextauth]/route";
import { redirect } from "next/navigation";
import Empty from "@/components/empty";

export default async function Home() {
const session = await getServerSession(authOptions);

if (!session) {
return redirect("/api/auth/signin");
}

const res = await fetch(
process.env.NEXTAUTH_URL + "/api/data/completions/rejected",
);
Expand Down
5 changes: 5 additions & 0 deletions app/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Dashboard from "@/components/dashboard";
import { getServerSession } from "next-auth";
import { authOptions } from "./api/auth/[...nextauth]/route";
import { StyledComponentsRegistry } from "@/components/styled-registry";
import { redirect } from "next/navigation";

export const metadata = {
title: "Human-in-the-loop",
Expand All @@ -14,6 +15,10 @@ export const metadata = {
export default async function RootLayout({ children }) {
const session = await getServerSession(authOptions);

if (!session) {
return redirect("/api/auth/signin");
}

return (
<html lang="en">
<body className={inter.className}>
Expand Down
13 changes: 13 additions & 0 deletions app/middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export { default } from "next-auth/middleware";

export const config = {
matcher: [
"/api-management",
"completions",
"/api/api-keys",
"/api/data/completions/approved",
"/api/data/completions/pending",
"/api/data/completions/rejected",
"/api/data/completions/review",
],
};
25 changes: 21 additions & 4 deletions components/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
Grommet,
grommet,
} from "grommet";
import { Logout, DocumentTest } from "grommet-icons";
import { Logout, DocumentTest, Contract } from "grommet-icons";
import { BRAND_HEX } from "../lib/config";
import { Logo } from "./logo";
import { usePathname, useRouter } from "next/navigation";
Expand Down Expand Up @@ -62,7 +62,7 @@ const SidebarButton = ({ icon, label, selected, ...rest }) => (
...rest.style,
whiteSpace: "nowrap",
height: "3rem",
paddingLeft: "3rem",
paddingLeft: "2rem",
flex: "unset",
background: selected ? BRAND_HEX : "transparent",
color: selected ? "white" : "unset",
Expand All @@ -73,9 +73,20 @@ const SidebarButton = ({ icon, label, selected, ...rest }) => (
const SidebarFooter = (props) => {
const { size, deviceType } = props;

const { push } = useRouter();
const pathname = usePathname();

const apiManagement = "/api-management";

if (deviceType === "mobile" || size === "small") {
return (
<Nav gap="small">
<Button
icon={<Contract />}
hoverIndicator={pathname !== apiManagement}
primary={pathname === apiManagement}
onClick={() => push(apiManagement)}
/>
<Button
icon={<Logout />}
hoverIndicator
Expand All @@ -91,6 +102,12 @@ const SidebarFooter = (props) => {

return (
<Nav>
<SidebarButton
icon={<Contract />}
label={"API Management"}
selected={pathname === apiManagement}
onClick={() => push(apiManagement)}
/>
<SidebarButton
icon={<Logout />}
label={"Logout"}
Expand Down Expand Up @@ -124,7 +141,7 @@ const MainNavigation = (props) => {
icon={<DocumentTest />}
hoverIndicator={!matchCompletions}
primary={matchCompletions}
onClick={() => push(completions)}
onClick={() => push(completions + "pending")}
/>
</Nav>
);
Expand All @@ -136,7 +153,7 @@ const MainNavigation = (props) => {
icon={<DocumentTest />}
label={"Completions"}
selected={matchCompletions}
onClick={() => push(completions)}
onClick={() => push(completions + "pending")}
/>
</Nav>
);
Expand Down
Loading

0 comments on commit e43fcf3

Please sign in to comment.