diff --git a/.github/workflows/wrangler-deploy.yml b/.github/workflows/wrangler-deploy.yml index 9284541..bc1cde6 100644 --- a/.github/workflows/wrangler-deploy.yml +++ b/.github/workflows/wrangler-deploy.yml @@ -21,7 +21,7 @@ jobs: name: Deploy golinks-next to staging environment: name: golinks-next/staging - url: ${{ steps.deploy.outputs.deployment-url }} + url: https://staging.go-next.andreijiroh.xyz/api/docs runs-on: ubuntu-latest needs: [paths-filter] if: needs.paths-filter.outputs.golinks-next == 'true' @@ -37,22 +37,20 @@ jobs: node-version: 20 - name: Install dependencies and generate Prisma client code run: | - cd apps/golinks-v2 yarn install - yarn prisma generate + yarn workspace @andreijiroh-dev/golinks-rewrite prisma generate - name: Deploy to Workers in staging id: deploy run: | - cd apps/golinks-v2 - yarn deploy:stg - git rev-parse | yarn wrangler secret put GIT_DEPLOY_COMMIT + yarn workspace @andreijiroh-dev/golinks-rewrite deploy:stg + git rev-parse HEAD | yarn workspace @andreijiroh-dev/golinks-rewrite wrangler secret put GIT_DEPLOY_COMMIT --env staging env: CLOUDFLARE_API_TOKEN: ${{ steps.dotenvx.outputs.WRANGLER_DEPLOY_TOKEN }} golinks-next-prod: name: Deploy golinks-next to production environment: name: golinks-next/production - url: https://staging.go-next.andreijiroh.xyz + url: https://go.andreijiroh.xyz/api/docs runs-on: ubuntu-latest needs: [paths-filter, golinks-next] if: needs.paths-filter.outputs.golinks-next == 'true' @@ -66,15 +64,14 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 20 - - name: Install dependencies + - name: Install dependencies and generate Prisma client code run: | yarn install - yarn prisma generate - - name: Deploy to Workers in production + yarn workspace @andreijiroh-dev/golinks-rewrite prisma generate + - name: Deploy to Workers in staging id: deploy run: | - cd apps/golinks-v2 - yarn deploy:prod - git rev-parse | yarn wrangler secret put GIT_DEPLOY_COMMIT + yarn workspace @andreijiroh-dev/golinks-rewrite deploy:prod + git rev-parse HEAD | yarn workspace @andreijiroh-dev/golinks-rewrite wrangler secret put GIT_DEPLOY_COMMIT --env production env: CLOUDFLARE_API_TOKEN: ${{ steps.dotenvx.outputs.WRANGLER_DEPLOY_TOKEN }} diff --git a/apps/golinks-v2/migrations/0006_api-keys-support.sql b/apps/golinks-v2/migrations/0006_api-keys-support.sql index 07945a6..39d5a6a 100644 --- a/apps/golinks-v2/migrations/0006_api-keys-support.sql +++ b/apps/golinks-v2/migrations/0006_api-keys-support.sql @@ -7,6 +7,16 @@ CREATE TABLE "SlackBotToken" ( "updated_on" DATETIME NOT NULL ); +-- CreateTable +CREATE TABLE "GitHubOAuthChallenge" ( + "id" TEXT NOT NULL PRIMARY KEY, + "challenge" TEXT NOT NULL, + "code" TEXT, + "metadata" TEXT NOT NULL, + "username" TEXT, + "userId" TEXT +); + -- CreateTable CREATE TABLE "ApiToken" ( "id" TEXT NOT NULL PRIMARY KEY, diff --git a/apps/golinks-v2/prisma/schema.prisma b/apps/golinks-v2/prisma/schema.prisma index 6e774f2..21c31c6 100644 --- a/apps/golinks-v2/prisma/schema.prisma +++ b/apps/golinks-v2/prisma/schema.prisma @@ -53,6 +53,15 @@ model SlackBotToken { updated_on DateTime @updatedAt() } +model GitHubOAuthChallenge { + id String @id @default(uuid()) + challenge String + code String? + metadata String + username String? + userId String? +} + model ApiToken { id String @id @default(uuid()) token String @unique @@ -61,7 +70,7 @@ model ApiToken { } model User { - id String @id + id String @id @default(uuid()) username String @unique email String? site_admin Boolean @default(false) diff --git a/apps/golinks-v2/src/api/github.ts b/apps/golinks-v2/src/api/github.ts index 7102206..c4812ed 100644 --- a/apps/golinks-v2/src/api/github.ts +++ b/apps/golinks-v2/src/api/github.ts @@ -1,22 +1,91 @@ +import { PrismaD1 } from "@prisma/adapter-d1"; +import { PrismaClient } from "@prisma/client"; import { Context } from "hono"; +import { generateSlug } from "lib/utils"; 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() + const appId = context.env.GITHUB_OAUTH_ID; + const appSecret = context.env.GITHUB_OAUTH_SECRET; + const redirect_uri = `${context.env.BASE_URL}/auth/github/callback`; + const { slack_id, slack_team, state, client_id, code, error, error_description, error_uri } = context.req.query(); + const adapter = new PrismaD1(context.env.golinks); + const prisma = new PrismaClient({ adapter }); - 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}`) - } + if (context.req.path == "/auth/github") { + if (client_id == "slack") { + const requestState = encodeURIComponent( + JSON.stringify({ + slack_id, + slack_team, + state, + }), + ); + const github = await prisma.gitHubOAuthChallenge.create({ + data: { + challenge: state, + metadata: JSON.stringify({ slack_team, slack_id }), + }, + }).catch(err => { + console.error(err) + return context.newResponse("Something gone wrong on the backend"); + }); + console.log(`[db] ${github}`); + return context.redirect( + `https://github.com/login/oauth/authorize?client_id=${appId}&redirect_uri=${redirect_uri}&state=${requestState}&scope=read:user,user:email`, + ); + } + } else if (context.req.path == "/auth/github/callback") { + let payload = { + code, + client_id: context.env.GITHUB_OAUTH_ID, + client_secret: context.env.GITHUB_OAUTH_SECRET, + redirect_uri, + state, + grant_type: "authorization_code", + }; + let formBody = Object.entries(payload) + .map(([key, value]) => encodeURIComponent(key) + "=" + encodeURIComponent(value)) + .join("&"); + if (error) { + const errorMsg = `OAuth flow was aborted with the following information: + error: ${error} + error_description: ${error_description} + error_uri ${error_uri}. + +Please try again authenicating. If you still having issues, please file a ticket at +https://go.andreijiroh.xyz/feedback/oauth-bug with the details above`; + return context.newResponse(errorMsg, 400); + } + const authToken = await fetch("https://github.com/login/oauth/access_token", { + method: "POST", + body: formBody, + headers: { + Accept: "application/json", + }, + }); + const { access_token } = await authToken.json() + const { state } = await context.req.query() + + if (!access_token) { + return context.newResponse("OAuth flow was aborted because the auth code is invalid.", 400) + } + const code = generateSlug(12) + fetch("https://api.github.com/user", { + headers: { + Authorization: `bearer ${access_token}` + } + }).then(result => { + const {username, id} = result.json() + const dbResult = prisma.gitHubOAuthChallenge.update({ + where: { + challenge: state + }, + data: { + code, + username, + userId: id + } + }) + }) + } } diff --git a/apps/golinks-v2/src/api/slack.ts b/apps/golinks-v2/src/api/slack.ts index 16bf73e..f140494 100644 --- a/apps/golinks-v2/src/api/slack.ts +++ b/apps/golinks-v2/src/api/slack.ts @@ -3,110 +3,110 @@ import { Context } from "hono"; import crypto from "node:crypto"; import { Buffer } from "node:buffer"; import { generateSlug } from "lib/utils"; +import { PrismaD1 } from "@prisma/adapter-d1"; +import { Prisma, PrismaClient } from "@prisma/client"; 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", + 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: "slack.go.andreijiroh.xyz - @ajhalili2006's golinks service in Slack", + text: "Request a link", emoji: true, }, + value: "request-link", + action_id: "golinks-bot-action", }, - { - 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: "Want to use me programmatically? You can request a API token using your GitHub account through the OAuth prompt.", }, - { - type: "section", + accessory: { + type: "button", 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: "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.", }, - { - type: "section", + accessory: { + type: "button", 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: "plain_text", + text: "Open API docs", + emoji: true, }, + value: "api-docs", + action_id: "golinks-bot-help", + url: `${context.env.BASE_URL}/api/docs`, }, - { - type: "section", - text: { + }, + { + type: "context", + elements: [ + { + text: "If something go wrong, or ping @ajhalili2006 on the fediverse.", 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); - } + ], + }, + ], + }; + return context.json(templateJson); +} /** * Handle requests for OAuth-based app installation @@ -195,8 +195,8 @@ export async function handleSlackCommand(context: Context) { if (command === "go") { if (data.text == "" || data.text == "help") { - return helpMessage(context, data) - } + return helpMessage(context, data); + } } else if (command == "ping") { const end = Date.now() - start; await console.log(`Pong with ${end}ms`); @@ -222,6 +222,11 @@ function validateTimestamp(timestamp: string): boolean { return delta >= 3 && delta <= 5; } +export async function handleInteractions(context: Context) { + const adapter = new PrismaD1(context.env.golinks); + const prisma = new PrismaClient({ adapter }); +} + async function createHashedBody(body: ArrayBuffer): Promise { const buffer = Buffer.from(body); const hash = crypto.createHash("sha256");