diff --git a/apps/golinks-v2/src/api/github.ts b/apps/golinks-v2/src/api/github.ts new file mode 100644 index 0000000..7102206 --- /dev/null +++ b/apps/golinks-v2/src/api/github.ts @@ -0,0 +1,22 @@ +import { Context } from "hono"; + +export async function githubAuth(context: Context) { + const appId = context.env.GITHUB_OAUTH_ID + const redirect_uri = `${context.env.BASE_URL}/auth/github/callback` + const { + slack_id, + slack_team, + state, + client_id, + code + } = context.req.query() + + if (client_id == "slack" && context.req.path == "/auth/github") { + const requestState = encodeURIComponent(JSON.stringify({ + slack_id, + slack_team, + state + })) + return context.redirect(`https://github.com/login/oauth?client_id=${appId}&redirect_uri=${redirect_uri}&state=${requestState}`) + } +} diff --git a/apps/golinks-v2/src/api/slack.ts b/apps/golinks-v2/src/api/slack.ts index a9edcd4..16bf73e 100644 --- a/apps/golinks-v2/src/api/slack.ts +++ b/apps/golinks-v2/src/api/slack.ts @@ -2,6 +2,111 @@ import fetch from "cross-fetch"; import { Context } from "hono"; import crypto from "node:crypto"; import { Buffer } from "node:buffer"; +import { generateSlug } from "lib/utils"; + +type SlackSlashCommand = { + token?: string, + team_id: string, + team_domain: string, + channel_id: string, + channel_name: string, + user_id: string, + user_name: string, + command: string, + text?: string, + is_enterprise_install: "true" | "false", + response_url: string, + trigger_id: string +} + +function helpMessage( + context: Context, + params: SlackSlashCommand) { + const challenge = `challenge_${generateSlug(24)}` + const githubAuthUrl = `${context.env.BASE_URL}/auth/github?client_id=slack&slack_team=${params.team_id}&slack_id=${params.user_id}&state=${challenge}` + const templateJson = { + blocks: [ + { + type: "header", + text: { + type: "plain_text", + text: "slack.go.andreijiroh.xyz - @ajhalili2006's golinks service in Slack", + emoji: true, + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: "I am the bot that uses to manage his golinks in Slack, although you can use me as a link shortener and to request custom golinks for approval.", + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: "Need to shorten a link, add a Discord or wikilink? Submit a request and you'll be notified via DMs if it's added.", + }, + accessory: { + type: "button", + text: { + type: "plain_text", + text: "Request a link", + emoji: true, + }, + value: "request-link", + action_id: "golinks-bot-action", + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: "Want to use me programmatically? You can request a API token using your GitHub account through the OAuth prompt.", + }, + accessory: { + type: "button", + text: { + type: "plain_text", + text: "Sign in to get token", + emoji: true, + }, + value: challenge, + action_id: "github-auth-challenge", + url: githubAuthUrl, + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: "Want to see the API docs and experiment with it? You can take a sneak peek.", + }, + accessory: { + type: "button", + text: { + type: "plain_text", + text: "Open API docs", + emoji: true, + }, + value: "api-docs", + action_id: "golinks-bot-help", + url: `${context.env.BASE_URL}/api/docs`, + }, + }, + { + type: "context", + elements: [ + { + text: "If something go wrong, or ping @ajhalili2006 on the fediverse.", + type: "mrkdwn", + }, + ], + }, + ], + }; + return context.json(templateJson); + } /** * Handle requests for OAuth-based app installation @@ -10,7 +115,7 @@ import { Buffer } from "node:buffer"; */ export async function slackOAuth(context: Context) { const appId = context.env.SLACK_OAUTH_ID || "5577525090644.7449518011266"; - const callback = context.env.SLACK_OAUTH_CALLBACK_URL || `${context.req.url}/callback`; + const callback = `${context.env.BASE_URL}/auth/slack/callback`; const scopes = "commands"; if (context.req.path == "/slack") { @@ -33,12 +138,12 @@ export async function slackOAuth(context: Context) { headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: formBody, }); - const result = await api.json() + const result = await api.json(); 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." }); + return context.json({ ok: true, result: "Successfully installed, run `/go help` in a channel or DM." }); } catch (err) { console.error(err); return context.json({ ok: false, error: "Something gone wrong" }); @@ -55,19 +160,19 @@ export async function slackOAuth(context: Context) { 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(); - const start = Date.now() + //const body = await context.req.arrayBuffer(); + const start = Date.now(); // 2. Get Request Headers - const timestamp = context.req.header("X-Slack-Request-Timestamp"); - const signature = context.req.header("X-Slack-Signature"); + //const timestamp = context.req.header("X-Slack-Request-Timestamp"); + //const signature = context.req.header("X-Slack-Signature"); const { command } = await context.req.param(); - const authToken = await context.req.query("token") - const data = context.req.parseBody(); // Parse body as JSON + const authToken = await context.req.query("token"); + const data = await context.req.parseBody(); // 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 })}`); + await console.log(`[slack-slash-commands] headers: ${context.req.header()}`); // 3. Validate Request Timestamp (optional) /*if (!validateTimestamp(timestamp)) { @@ -89,11 +194,13 @@ export async function handleSlackCommand(context: Context) { */ if (command === "go") { - return context.newResponse("Still working in it.", 200, { "Content-Type": "text/plain" }); + if (data.text == "" || data.text == "help") { + return helpMessage(context, data) + } } else if (command == "ping") { - const end = Date.now() - start - await console.log(`Pong with ${end}ms`) - return context.newResponse(`Pong with ${end}ms response time in backend`) + const end = Date.now() - start; + await console.log(`Pong with ${end}ms`); + return context.newResponse(`Pong with ${end}ms response time in backend`); } return context.newResponse("Unsupported command"); } @@ -108,15 +215,15 @@ function validateTimestamp(timestamp: string): boolean { if (!timestamp) { return false; } - const currentTimestamp = Date.now() - const requestTimestamp = new Date(timestamp).getMilliseconds() + const currentTimestamp = Date.now(); + const requestTimestamp = new Date(timestamp).getMilliseconds(); const delta = Math.abs(currentTimestamp - requestTimestamp) / (1000 * 60); - console.log(`[slack-slash-commands] current: ${currentTimestamp}, request: ${requestTimestamp}, delta: ${delta}`) + console.log(`[slack-slash-commands] current: ${currentTimestamp}, request: ${requestTimestamp}, delta: ${delta}`); return delta >= 3 && delta <= 5; } async function createHashedBody(body: ArrayBuffer): Promise { - const buffer = Buffer.from(body) + const buffer = Buffer.from(body); const hash = crypto.createHash("sha256"); hash.update(buffer); return hash.digest("hex"); diff --git a/apps/golinks-v2/src/index.tsx b/apps/golinks-v2/src/index.tsx index 80b1e12..2020630 100644 --- a/apps/golinks-v2/src/index.tsx +++ b/apps/golinks-v2/src/index.tsx @@ -21,6 +21,7 @@ import { CommitHash, PingPong } from "api/meta"; import { prettyJSON } from "hono/pretty-json"; import { generateNewIssueUrl, handleOldUrls } from "lib/utils"; import { handleSlackCommand, slackOAuth } from "api/slack"; +import { githubAuth } from "api/github"; // Start a Hono app const app = new Hono<{ Bindings: EnvBindings }>(); @@ -78,6 +79,8 @@ app.post("/api/slack/slash-commands/:command", async (c) => handleSlackCommand(c // Slack bot and slash commands app.get("/slack", async (c) => slackOAuth(c)); app.get("/auth/slack/callback", async (c) => slackOAuth(c)); +app.get("/auth/github", async (c) => githubAuth(c)) +app.get("/auth/github/auth", async (c) => githubAuth(c)) app.get("/", (c) => { return c.redirect(homepage); diff --git a/apps/golinks-v2/src/types.ts b/apps/golinks-v2/src/types.ts index b62c7e9..513e010 100644 --- a/apps/golinks-v2/src/types.ts +++ b/apps/golinks-v2/src/types.ts @@ -40,10 +40,12 @@ export interface Env { GITHUB_OAUTH_SECRET: string; golinks: D1Database; ADMIN_KEY: string; + BASE_URL: string; } export type EnvBindings = { golinks: D1Database; + BASE_URL: string DEPLOY_ENV: "production" | "staging" | "development"; ADMIN_KEY: string; GIT_DEPLOY_COMMIT: string; diff --git a/apps/golinks-v2/wrangler.toml b/apps/golinks-v2/wrangler.toml index 5c651d9..adf81db 100644 --- a/apps/golinks-v2/wrangler.toml +++ b/apps/golinks-v2/wrangler.toml @@ -16,7 +16,7 @@ placement = { mode = "smart" } compatibility_flags = [ "nodejs_compat" ] # Please do not leak your secrets here. -vars = { DEPLOY_ENV = "development", ADMIN_KEY = "gostg_localdev-null" } +vars = { DEPLOY_ENV = "development", ADMIN_KEY = "gostg_localdev-null", BASE_URL = "https://stellapent-cier.fawn-cod.ts.net" } d1_databases = [ { binding = "golinks", database_name = "golinks-db-preview", database_id = "0b415a64-cc61-4c2b-a7c8-2d223f18e996", migrations_dir = "migrations" }, @@ -24,7 +24,7 @@ d1_databases = [ [env.staging] workers_dev = true -vars = { DEPLOY_ENV = "staging" } +vars = { DEPLOY_ENV = "staging", BASE_URL = "https://staging.go-next.andreijiroh.xyz" } d1_databases = [ { binding = "golinks", database_name = "golinks-db-preview", database_id = "0b415a64-cc61-4c2b-a7c8-2d223f18e996", migrations_dir = "migrations" }, ] @@ -35,7 +35,7 @@ routes = [ [env.production] workers_dev = true -vars = { DEPLOY_ENV = "production" } +vars = { DEPLOY_ENV = "production", BASE_URL = "https://go.andreijiroh.xyz" } d1_databases = [ { binding = "golinks", database_name = "golinks-db", database_id = "63e746f4-40de-4f8c-a3e5-7969cde040b3", migrations_dir = "migrations" }, ]