From ea02bdbe3ece76eb22b5c81f81fbe20ddf6105c8 Mon Sep 17 00:00:00 2001 From: Andrei Jiroh Halili Date: Sat, 10 Aug 2024 16:24:44 +0800 Subject: [PATCH] feat(golinks): :sparkles: add new API endpoints and stuff Signed-off-by: Andrei Jiroh Halili --- apps/golinks-v2/prisma/schema.prisma | 20 ----- apps/golinks-v2/src/api/debug.ts | 2 + apps/golinks-v2/src/api/golinks.ts | 106 +++++++++++++++++++-------- apps/golinks-v2/src/api/slack.ts | 20 +++++ apps/golinks-v2/src/index.tsx | 16 +++- 5 files changed, 110 insertions(+), 54 deletions(-) diff --git a/apps/golinks-v2/prisma/schema.prisma b/apps/golinks-v2/prisma/schema.prisma index d12f675..accf920 100644 --- a/apps/golinks-v2/prisma/schema.prisma +++ b/apps/golinks-v2/prisma/schema.prisma @@ -45,26 +45,6 @@ model WikiLinks { updated_on DateTime @updatedAt() } -model SlackBotToken { - teamId String @id @unique - enterprise_install Boolean @default(false) - token String - created_on DateTime @default(now()) - updated_on DateTime @updatedAt() -} - -model GitHubOAuthChallenge { - id String @id @default(uuid()) - challenge String - code String? - metadata String - username String? - userId String? - created_on DateTime @default(now()) - updated_on DateTime @updatedAt() - expires_on DateTime -} - model ApiToken { id String @id @default(uuid()) token String @unique diff --git a/apps/golinks-v2/src/api/debug.ts b/apps/golinks-v2/src/api/debug.ts index 7b7db1e..6a9dd48 100644 --- a/apps/golinks-v2/src/api/debug.ts +++ b/apps/golinks-v2/src/api/debug.ts @@ -51,6 +51,8 @@ export class debugApiGenerateJwt extends OpenAPIRoute { export class debugApiGetBindings extends OpenAPIRoute { schema = { summary: "Show all Worker bindings associated with this instance, including secrets.", + description: "Due to nature of this endpoint, you need to have the value of `ADMIN_KEY` secret on hand for this request.", + tags: ["debug"], security: [ {userApiKey: []} ] diff --git a/apps/golinks-v2/src/api/golinks.ts b/apps/golinks-v2/src/api/golinks.ts index 7266be8..30e4c08 100644 --- a/apps/golinks-v2/src/api/golinks.ts +++ b/apps/golinks-v2/src/api/golinks.ts @@ -1,7 +1,7 @@ -import { OpenAPIRoute, Num, Bool, Str, contentJson } from "chanfana"; +import { OpenAPIRoute, Num, Bool, Str, contentJson, Obj } from "chanfana"; import { z } from "zod"; import { GoLinks } from "types"; -import { addGoLink, getGoLinks, getLink, updateGoLink } from "lib/db"; +import { addGoLink, deleteGoLink, getGoLinks, getLink, updateGoLink } from "lib/db"; import { Context } from "hono"; import { generateSlug } from "lib/utils"; @@ -24,9 +24,6 @@ export class GoLinkCreate extends OpenAPIRoute { }, }, security: [ - { - adminApiKey: [], - }, { userApiKey: [], }, @@ -48,8 +45,9 @@ export class GoLinkCreate extends OpenAPIRoute { content: { "application/json": { schema: z.object({ - success: Bool({ default: false }).default(false), - error: Str({ default: "Existing entry found (error code: PrismaClientKnownRequestError:P2002)" }), + ok: Bool({ default: false }).default(false), + result: Obj({}), + error: Str({ default: "That slug exists on the backend." }), }), }, }, @@ -63,19 +61,16 @@ export class GoLinkCreate extends OpenAPIRoute { console.log(`[golinks-api] received body for link creation ${JSON.stringify(linkToCreate)}`); const slug = linkToCreate.slug !== undefined ? linkToCreate.slug : generateSlug(12); - const result = await addGoLink(c.env.golinks, slug, linkToCreate.targetUrl, "golinks"); - - if (result) { - return c.newResponse( - JSON.stringify({ - success: true, - result: result, - }), - 200, - { "Content-Type": "application/json" }, - ); - } else { - return c.newResponse("Something went wrong", 400); + try { + const result = await addGoLink(c.env.golinks, slug, linkToCreate.targetUrl, "golinks"); + return c.json({ ok: true, result }); + } catch ({ code, meta, message, clientVersion }) { + Error(`[prisma] ${code} ${message} (${JSON.stringify(meta)})`); + if (code == "P2002") { + return c.json({ ok: false, result: {}, error: "That slug exists on the backend." }); + } else { + return c.json({ ok: false, result: {}, error: "Something went wrong on our side." }); + } } } } @@ -99,9 +94,6 @@ export class GoLinkList extends OpenAPIRoute { }), }, security: [ - { - adminApiKey: [], - }, { userApiKey: [], }, @@ -139,12 +131,16 @@ export class GoLinkList extends OpenAPIRoute { export class GoLinkUpdate extends OpenAPIRoute { schema = { summary: "Update a golink", + description: `\ +Updating a golink's slug and/or its target URL requires admin level permissions. + +If you generated a random slug or requested a new golink as a regular user, please contact Andrei Jiroh to get it sorted.`, tags: ["golinks"], parameters: [ { name: "slug", in: "path", - description: "Slug name of the golink" + description: "Slug name of the golink to be changed" }, ], request: { @@ -152,17 +148,20 @@ export class GoLinkUpdate extends OpenAPIRoute { content: { "application/json": { schema: z.object({ - slug: Str({ required: false }), - targetUrl: Str({ example: "https://example.com" }), + slug: Str({ + required: false, + description: "The desired slug for the golinks." + }), + targetUrl: Str({ + example: "https://example.com", + description: "The target URL of the golink to redirect into" + }), }), }, }, }, }, security: [ - { - adminApiKey: [], - }, { userApiKey: [], }, @@ -213,7 +212,15 @@ export class GoLinkInfo extends OpenAPIRoute { ], responses: { "200": { - + description: "Shows a information about a golink", + content: { + "application/json": { + schema: z.object({ + ok: Bool({default: true}), + result: GoLinks + }), + }, + } } } }; @@ -221,8 +228,45 @@ export class GoLinkInfo extends OpenAPIRoute { const { slug } = context.req.param(); const result = await getLink(context.env.golinks, slug, "golinks"); if (!result) { - return context.json({ ok: false, error: "Not found" }, 404); + return context.json({ ok: false, result: {}, error: "Not found" }, 404); } return context.json({ ok: true, result }); } } + +export class GoLinkDelete extends OpenAPIRoute { + schema = { + tags: ["golinks"], + summary: "Deletes a existing golink", + description: `\ +This API endpoint is reserved to those with admin permissions due to abuse. + +If you are requesting to delete a golink for legal reasons, please contact Andrei Jiroh to get it sorted.`, + parameters: [ + { + name: "slug", + in: "path", + description: "Slug name of the golink to be deleted.", + }, + ], + security: [ + { + userApiKey: [], + }, + ], + }; + + async handle(c: Context) { + const { slug } = c.req.param(); + try { + await deleteGoLink(c.env.golinks, slug, "golinks"); + return c.json({ok: true, result: { deleted: true }}) + } catch({code}) { + if (code == "P2025") { + return c.json({ok: false, result: {}, error: "Either that slug is deleted on server-side or does not exist."}, 400) + } else { + return c.json({ok: false, result: {}, error: "Something went wrong on our side."}, 500) + } + } + } +} diff --git a/apps/golinks-v2/src/api/slack.ts b/apps/golinks-v2/src/api/slack.ts index 49c8712..c7acce8 100644 --- a/apps/golinks-v2/src/api/slack.ts +++ b/apps/golinks-v2/src/api/slack.ts @@ -12,6 +12,7 @@ import { Installation, InstallationQuery } from "@slack/oauth"; import { Bool, OpenAPIRoute, Str } from "chanfana"; import { adminApiKey } from "lib/constants"; import { z } from "zod"; +import { EnvBindings } from "types"; type SlackSlashCommand = { token?: string; @@ -279,6 +280,11 @@ export async function slackOAuthCallback(context: Context) { try { const api = await slackOAuthExchange(payload); const result = await api.json(); + const testAuthData = await fetch("https://slakc.com/api/auth.test", { + headers: { + Authorization: `bearer ${result.access_token}` + } + }) console.log(`[slack-oauth] result: ${JSON.stringify(result)} (${api.status})`); @@ -491,6 +497,20 @@ export class debugApiTestSlackBotToken extends OpenAPIRoute { } } +export class debugApiListSlackAppInstalls extends OpenAPIRoute { + schema = { + tags: ["debug"], + summary: "Get a list of Slack app installations recorded from KV storage for bot tokens", + security: [{userApiKey: []}] + } + async handle(context: Context) { + const env: EnvBindings = context.env + const result = await env.slackBotTokens.list() + + return context.json({ok: true, result}) + } +} + /** * Validate request timestamps to ensure that we don't received forged requests * within 3-5 minutes diff --git a/apps/golinks-v2/src/index.tsx b/apps/golinks-v2/src/index.tsx index 1c88a89..4578ae2 100644 --- a/apps/golinks-v2/src/index.tsx +++ b/apps/golinks-v2/src/index.tsx @@ -1,11 +1,10 @@ -import { GoLinkCreate, GoLinkInfo, GoLinkList, GoLinkUpdate } from "api/golinks"; +import { GoLinkCreate, GoLinkDelete, GoLinkInfo, GoLinkList, GoLinkUpdate } from "api/golinks"; import { fromHono } from "chanfana"; import { Context, Hono } from "hono"; import { cors } from "hono/cors"; import { EnvBindings } from "./types"; import { getDiscordInvite, getLink } from "lib/db"; import { - adminApiKey, contact, getWorkersDashboardUrl, homepage, @@ -24,6 +23,7 @@ import { prettyJSON } from "hono/pretty-json"; import { generateNewIssueUrl, handleOldUrls } from "lib/utils"; import { debugApiGetSlackBotToken, + debugApiListSlackAppInstalls, debugApiTestSlackBotToken, handleSlackCommand, handleSlackInteractivity, @@ -55,7 +55,15 @@ app.use( credentials: true, }), ); + +/** + * Array of strings containing priviledged HTTP methods when calling the API. + */ const privilegedMethods = ["POST", "PUT", "PATCH", "DELETE"] +/** + * In addition to the values of `priviledgedMethods`, we also add `GET` requests + * for debug API endpoints. + */ const debugApiMethods = [ "GET", ...privilegedMethods ] app.on(debugApiMethods, "/api/debug/*", async (c, next) => { const bearer = bearerAuth({ @@ -73,6 +81,7 @@ app.on("POST", "/api/slack/*", async (c, next) => { app.on(privilegedMethods, "/api/*", async (c, next) => { const bearer = bearerAuth({ verifyToken: async (token: string, context: Context) => { + await console.log(`[auth]: received token - ${token}`) if (token == context.env.ADMIN_KEY) { return true } @@ -104,7 +113,6 @@ const openapi = fromHono(app, { }, }); -openapi.registry.registerComponent("securitySchemes", "adminApiKey", adminApiKey); openapi.registry.registerComponent("securitySchemes", "userApiKey", userApiKey); // Register OpenAPI endpoints in this section @@ -112,6 +120,7 @@ openapi.get("/api/links", GoLinkList); openapi.post("/api/links", GoLinkCreate); openapi.get("/api/links/:slug", GoLinkInfo) openapi.put("/api/links/:slug", GoLinkUpdate); +openapi.delete("/api/links/:slug", GoLinkDelete) // category:wikilinks openapi.post("/api/wikilinks", WikiLinkCreate) // category:discord-invites @@ -123,6 +132,7 @@ openapi.get("/api/commit", CommitHash); // category: debug openapi.get("/api/debug/slack/bot-token", debugApiGetSlackBotToken); openapi.get("/api/debug/slack/auth-test", debugApiTestSlackBotToken); +openapi.get("/api/debug/slack/installs", debugApiListSlackAppInstalls) openapi.get("/api/debug/bindings", debugApiGetBindings) // Undocumented API endpoints: Slack integration -- knotmirror 0.0.1