Skip to content

Commit

Permalink
feat(golinks): update backend code for Slack slash commands
Browse files Browse the repository at this point in the history
Signed-off-by: Andrei Jiroh Halili <[email protected]>
  • Loading branch information
ajhalili2006 committed Jul 21, 2024
1 parent 1ba9961 commit 5a18940
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
yarnPath: .yarn/releases/yarn-4.2.2.cjs
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.3.1.cjs
76 changes: 70 additions & 6 deletions apps/golinks-v2/src/api/slack.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fetch from "cross-fetch";
import { Context } from "hono";
import crypto from "node:crypto";

/**
* Handle requests for OAuth-based app installation
Expand Down Expand Up @@ -32,17 +33,80 @@ export async function slackOAuth(context: Context) {
body: formBody,
});
const result = await api.json()
return context.json({ ok: true, result: result });
console.log(`[slack-oauth] result: ${JSON.stringify(result)} (${api.status})`);
if (result.ok == false) {
return context.json({ ok: false, error: result.error }, api.status);
}
return context.json({ ok: true, result: "Successfully installed." });
} catch (err) {
console.error(err);
return context.json({ ok: false, error: "Something gone wrong" });
}
}
}

export function handleSlackCommand(context: Context) {
const headers = context.req.header()
const data = context.req.parseBody()
console.log(JSON.stringify(data))
return context.newResponse("Still working in it.")
/**
* Function handler for `/api/slack/slash-commands/:command` POST requests
* on Hono.
* @param context The `context` object from Hono.
* @returns
*/
export async function handleSlackCommand(context: Context) {
// 1. Get Signing Secret and Request Body
const signingSecret = context.env.SLACK_SIGNING_SECRET; // Access signing secret from env
const body = await context.req.arrayBuffer();

// 2. Get Request Headers
const timestamp = context.req.header("X-Slack-Request-Timestamp");
const signature = context.req.header("X-Slack-Signature");

// 3. Validate Request Timestamp (optional)
if (!validateTimestamp(timestamp)) {
return new Response("Request is too old.", { status: 403 });
}

// 4. Calculate Base String
const baseString = `v0:${timestamp}:${await createHashedBody(body)}`;

// 5. Calculate Expected Signature
const expectedSignature = `sha256=${crypto.createHmac("sha256", signingSecret).update(baseString).digest("hex")}`;
await console.log(`[slack-slash-commands]: expected: ${expectedSignature}, received: ${signature}`);

// 6. Validate Signature
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
return new Response("Invalid request signature.", { status: 403 });
}

const { command } = await context.req.param();
const data = JSON.parse(await context.req.text()); // Parse body as JSON

await console.log(`[slack-slash-commands] params: ${JSON.stringify(data)}`);
await console.log(`[slack-slash-commands] headers: ${JSON.stringify({ timestamp, command })}`);

if (command === "go") {
return context.newResponse("Still working in it.", 200, { "Content-Type": "text/plain" });
}
return context.newResponse("Unsupported command");
}

/**
* Validate request timestamps to ensure that we don't received forged requests
* within 3-5 minutes
* @param timestamp The request timestamp in string form
* @returns
*/
function validateTimestamp(timestamp: string): boolean {
if (!timestamp) {
return false;
}
const currentTimestamp = Date.now()
const requestTimestamp = new Date(timestamp)
const delta = Math.abs(currentTimestamp - requestTimestamp) / (1000 * 60);
return delta >= 3 && delta <= 5;
}

async function createHashedBody(body: ArrayBuffer): Promise<String> {
const hash = crypto.createHash("sha256");
hash.update(body);
return hash.digest("hex");
}
2 changes: 1 addition & 1 deletion apps/golinks-v2/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ openapi.get("/api/discord-invites", DiscordInviteLinkList);
openapi.post("/api/discord-invites", DiscordInviteLinkCreate);
openapi.get("/api/ping", PingPong);
openapi.get("/api/commit", CommitHash);
app.post("/api/slack-slash-commands/:command", handleSlackCommand)
app.post("/api/slack/slash-commands/:command", async (c) => handleSlackCommand(c))

// Slack bot and slash commands
app.get("/slack", async(c) => slackOAuth(c));
Expand Down
4 changes: 4 additions & 0 deletions apps/golinks-v2/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export async function adminApiKeyAuth(c: Context, next: Next) {
return await next();
}

if (c.req.path.startsWith("/api/slack")) {
return await next()
}

const adminApiKey = c.env.ADMIN_KEY;
const apiKeyHeader = c.req.header("X-Golinks-Admin-Key");
console.debug(`[auth] ${adminApiKey}:${apiKeyHeader}`);
Expand Down
12 changes: 12 additions & 0 deletions apps/golinks-v2/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export const DiscordInvites = z.object({
export interface Env {
DEPLOY_ENV: "production" | "staging" | "development";
GIT_DEPLOY_COMMIT: string;
SLACK_OAUTH_ID: string
SLACK_OAUTH_SECRET: string,
SLACK_OAUTH_CALLBACK_URL: string,
SLACK_SIGNING_SECRET: string,
GITHUB_OAUTH_ID: string,
GITHUB_OAUTH_SECRET: string,
golinks: D1Database;
ADMIN_KEY: string;
}
Expand All @@ -42,4 +48,10 @@ export type EnvBindings<Env> = {
DEPLOY_ENV: "production" | "staging" | "development";
ADMIN_KEY: string;
GIT_DEPLOY_COMMIT: string;
SLACK_OAUTH_ID: string;
SLACK_OAUTH_SECRET: string;
SLACK_OAUTH_CALLBACK_URL: string;
SLACK_SIGNING_SECRET: string;
GITHUB_OAUTH_ID: string;
GITHUB_OAUTH_SECRET: string;
};
1 change: 1 addition & 0 deletions apps/golinks-v2/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ compatibility_date = "2024-07-12" # TODO: Update this once a month
account_id = "cf0bd808c6a294fd8c4d8f6d2cdeca05"

placement = { mode = "smart" }
compatibility_flags = [ "nodejs_compat" ]

# Please do not leak your secrets here.
vars = { DEPLOY_ENV = "development", ADMIN_KEY = "gostg_localdev-null" }
Expand Down

0 comments on commit 5a18940

Please sign in to comment.