-
Notifications
You must be signed in to change notification settings - Fork 0
[oauth] add bitbucket connection #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # Changelog | ||
|
|
||
| All notable changes to BitIssues are documented here. | ||
| Format follows [Keep a Changelog](https://keepachangelog.com/) conventions. | ||
|
|
||
| ## [0.15.3] - 2026-08-25 | ||
|
|
||
| ### Bug Fixes | ||
|
|
||
| - **Webhook signature header fallback** — webhook handler now accepts both `X-Hub-Signature` and `X-Hub-Signature-256` headers, improving compatibility with different Bitbucket webhook configurations | ||
|
Comment on lines
+6
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Record the Bitbucket OAuth feature in the correct release section. This changelog documents only the webhook header fix. It omits the Bitbucket OAuth feature added by this PR. The 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -85,6 +85,9 @@ Here's why BitIssues exists: | |||||||||||||
| - **Rich Filtering & Sorting** — tasks filterable by project, author, assignee, status, priority, date range | ||||||||||||||
| - **Dashboard Queries** — quick access to tasks assigned to or created by the current user | ||||||||||||||
| - **Swagger/OpenAPI** — auto-generated API docs at `/api/v1/docs` | ||||||||||||||
| - **Bitbucket Webhooks** — commit messages (e.g. `fixes #5`) automatically transition task statuses with bot-authored comments | ||||||||||||||
| - **Bitbucket OAuth** — admin "Connect with Bitbucket" flow to manage repository webhooks; tokens encrypted at rest (AES-GCM) | ||||||||||||||
| - **Prometheus Metrics** — built-in metrics endpoint for monitoring | ||||||||||||||
|
|
||||||||||||||
| <p align="right">(<a href="#readme-top">back to top</a>)</p> | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -246,21 +249,28 @@ A complete API reference with request/response examples is available in [`reques | |||||||||||||
|
|
||||||||||||||
| Configuration is loaded from environment variables with optional YAML override via `CONFIG_PATH`. | ||||||||||||||
|
|
||||||||||||||
| | Variable | Default | Description | | ||||||||||||||
| | --------------------------- | --------------------------- | -------------------------------------- | | ||||||||||||||
| | `DATABASE__URL` | `mariadb://bit-issues:...` | Database connection string | | ||||||||||||||
| | `JWT__SECRET` | `secret` | JWT signing key | | ||||||||||||||
| | `JWT__ACCESS_TTL` | `15m` | Access token lifetime | | ||||||||||||||
| | `STORAGE__URL` | `s3://bucket/prefix?...` | S3 storage URL | | ||||||||||||||
| | `STORAGE__LINKS_TTL` | `15m` | Presigned URL lifetime | | ||||||||||||||
| | `ATTACHMENTS__MAX_SIZE` | `10485760` | Max file size in bytes (10 MB) | | ||||||||||||||
| | `HTTP__ADDRESS` | `127.0.0.1:3000` | Server listen address | | ||||||||||||||
| | `AWS_ACCESS_KEY_ID` | — | S3 access key | | ||||||||||||||
| | `AWS_SECRET_ACCESS_KEY` | — | S3 secret key | | ||||||||||||||
| | `AWS_REGION` | — | S3 region | | ||||||||||||||
| | `WEBAUTHN__RP_DISPLAY_NAME` | `BitIssues` | Display name shown during registration | | ||||||||||||||
| | `WEBAUTHN__RP_ID` | `localhost` | Relying Party ID (domain) | | ||||||||||||||
| | `WEBAUTHN__RP_ORIGINS` | `["http://localhost:5173"]` | Allowed origins JSON array | | ||||||||||||||
| | Variable | Default | Description | | ||||||||||||||
| | ----------------------------- | --------------------------- | --------------------------------------------------- | | ||||||||||||||
| | `DATABASE__URL` | `mariadb://bit-issues:...` | Database connection string | | ||||||||||||||
| | `JWT__SECRET` | `secret` | JWT signing key | | ||||||||||||||
| | `JWT__ACCESS_TTL` | `15m` | Access token lifetime | | ||||||||||||||
| | `JWT__REFRESH_TTL` | `168h` | Refresh token lifetime (7 days) | | ||||||||||||||
| | `STORAGE__URL` | `s3://bucket/prefix?...` | S3 storage URL | | ||||||||||||||
| | `STORAGE__LINKS_TTL` | `15m` | Presigned URL lifetime | | ||||||||||||||
| | `ATTACHMENTS__MAX_SIZE` | `10485760` | Max file size in bytes (10 MB) | | ||||||||||||||
| | `HTTP__ADDRESS` | `127.0.0.1:3000` | Server listen address | | ||||||||||||||
| | `CACHE__URL` | `memory://` | Cache backend (`memory://` or `redis://...`) | | ||||||||||||||
| | `AWS_ACCESS_KEY_ID` | — | S3 access key | | ||||||||||||||
| | `AWS_SECRET_ACCESS_KEY` | — | S3 secret key | | ||||||||||||||
| | `AWS_REGION` | — | S3 region | | ||||||||||||||
| | `WEBAUTHN__RP_DISPLAY_NAME` | `BitIssues` | Display name shown during registration | | ||||||||||||||
| | `WEBAUTHN__RP_ID` | `localhost` | Relying Party ID (domain) | | ||||||||||||||
| | `WEBAUTHN__RP_ORIGINS` | `["http://localhost:5173"]` | Allowed origins JSON array | | ||||||||||||||
| | `WEBHOOKS__SECRET` | — | HMAC-SHA256 secret for webhook payload verification | | ||||||||||||||
| | `WEBHOOKS__BOT_USER_EMAIL` | `bot@bitissues.local` | Email of the bot that posts comments | | ||||||||||||||
| | `OAUTH__CLIENT_ID` | — | Bitbucket OAuth consumer key | | ||||||||||||||
| | `OAUTH__CLIENT_SECRET` | — | Bitbucket OAuth consumer secret | | ||||||||||||||
| | `OAUTH__TOKEN_ENCRYPTION_KEY` | — | AES-256 key for encrypting OAuth tokens at rest | | ||||||||||||||
|
Comment on lines
+271
to
+273
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Document that OAuth is disabled when these values are empty.
Suggested documentation change-| `OAUTH__CLIENT_ID` | — | Bitbucket OAuth consumer key |
-| `OAUTH__CLIENT_SECRET` | — | Bitbucket OAuth consumer secret |
-| `OAUTH__TOKEN_ENCRYPTION_KEY` | — | AES-256 key for encrypting OAuth tokens at rest |
+| `OAUTH__CLIENT_ID` | `empty (disabled)` | Bitbucket OAuth consumer key |
+| `OAUTH__CLIENT_SECRET` | `empty (disabled)` | Bitbucket OAuth consumer secret |
+| `OAUTH__TOKEN_ENCRYPTION_KEY` | `empty (disabled)` | AES-256 key for encrypting OAuth tokens at rest |📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| ### Development Commands | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -295,7 +305,7 @@ Pre-built multi-arch Docker images are published to **GHCR** (`ghcr.io/bit-issue | |||||||||||||
| - [x] WebAuthn/passkey authentication | ||||||||||||||
| - [x] File attachments via S3 presigned URLs | ||||||||||||||
| - [ ] Email notifications | ||||||||||||||
| - [ ] Webhook integration | ||||||||||||||
| - [x] Webhook integration | ||||||||||||||
| - [ ] Multi-language support | ||||||||||||||
| - [ ] Kanban board view | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -337,6 +347,7 @@ Project Link: [https://github.com/bit-issues/backend](https://github.com/bit-iss | |||||||||||||
|
|
||||||||||||||
| * [Fiber](https://gofiber.io) | ||||||||||||||
| * [Uber Fx](https://github.com/uber-go/fx) | ||||||||||||||
| * [go-core-fx](https://github.com/go-core-fx) | ||||||||||||||
| * [uptrace/bun](https://github.com/uptrace/bun) | ||||||||||||||
| * [pressly/goose](https://github.com/pressly/goose) | ||||||||||||||
| * [MinIO](https://min.io) | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| @client_id={{$dotenv BITBUCKET__CLIENT_ID}} | ||
| @client_secret={{$dotenv BITBUCKET__CLIENT_SECRET}} | ||
| @code={{$dotenv BITBUCKET__CODE}} | ||
|
|
||
| ### | ||
| POST https://bitbucket.org/site/oauth2/access_token HTTP/1.1 | ||
| Authorization: Basic {{client_id}}:{{client_secret}} | ||
| Content-Type: application/x-www-form-urlencoded | ||
|
|
||
| grant_type=authorization_code&code={{code}} | ||
|
capcom6 marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { apiRequest } from './client' | ||
| import type { | ||
| BitbucketOAuthAuthorizeResponse, | ||
| BitbucketOAuthStatus, | ||
| } from '$lib/types/api' | ||
|
|
||
| export function getBitbucketOAuthStatus(): Promise<BitbucketOAuthStatus> { | ||
| return apiRequest<BitbucketOAuthStatus>('GET', '/oauth/bitbucket/status') | ||
| } | ||
|
|
||
| export function getBitbucketOAuthAuthorizeUrl(): Promise<BitbucketOAuthAuthorizeResponse> { | ||
| return apiRequest<BitbucketOAuthAuthorizeResponse>( | ||
| 'GET', | ||
| '/oauth/bitbucket/authorize', | ||
| ) | ||
| } | ||
|
|
||
| export function disconnectBitbucketOAuth(): Promise<void> { | ||
| return apiRequest<void>('POST', '/oauth/bitbucket/disconnect') | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| <script lang="ts"> | ||
| import { Button } from "$lib/components/ui/button"; | ||
| import * as Card from "$lib/components/ui/card"; | ||
| import * as Badge from "$lib/components/ui/badge"; | ||
| import * as Dialog from "$lib/components/ui/dialog"; | ||
| import { | ||
| disconnectBitbucketOAuth, | ||
| getBitbucketOAuthAuthorizeUrl, | ||
| getBitbucketOAuthStatus, | ||
| } from "$lib/api/oauth"; | ||
| import { toast } from "$lib/toast"; | ||
| import type { BitbucketOAuthStatus } from "$lib/types/api"; | ||
|
|
||
| let status = $state<BitbucketOAuthStatus | null>(null); | ||
| let loading = $state(true); | ||
| let loadError = $state(""); | ||
| let busy = $state(false); | ||
| let showDisconnectDialog = $state(false); | ||
|
|
||
| let connected = $derived(status?.connected === true); | ||
|
|
||
| let badgeLabel = $derived(connected ? "Connected" : "Disconnected"); | ||
| let badgeColor = $derived( | ||
| connected | ||
| ? "border-transparent bg-green-100 text-green-700 dark:bg-green-300/15 dark:text-green-300" | ||
| : "border-transparent bg-gray-100 text-gray-600 dark:bg-gray-300/15 dark:text-gray-300", | ||
| ); | ||
|
|
||
| function formatDate(iso?: string): string { | ||
| if (!iso) return "-"; | ||
| const d = new Date(iso); | ||
| return Number.isNaN(d.getTime()) ? iso : d.toLocaleString(); | ||
| } | ||
|
|
||
| function loadStatus() { | ||
| loading = true; | ||
| loadError = ""; | ||
| getBitbucketOAuthStatus() | ||
| .then((res) => { | ||
| status = res; | ||
| }) | ||
| .catch((e: Error) => { | ||
| status = null; | ||
| loadError = e?.message || "Failed to load Bitbucket connection status"; | ||
| }) | ||
| .finally(() => { | ||
| loading = false; | ||
| }); | ||
| } | ||
|
|
||
| $effect(loadStatus); | ||
|
|
||
| async function handleConnect() { | ||
| if (busy) return; | ||
| busy = true; | ||
| try { | ||
| const { url } = await getBitbucketOAuthAuthorizeUrl(); | ||
| window.location.assign(url); | ||
| } catch (e: any) { | ||
| toast.error(e?.message || "Failed to start Bitbucket connection"); | ||
| busy = false; | ||
| } | ||
| } | ||
|
|
||
| async function handleDisconnect() { | ||
| if (busy) return; | ||
| busy = true; | ||
| try { | ||
| await disconnectBitbucketOAuth(); | ||
| status = { connected: false }; | ||
| showDisconnectDialog = false; | ||
| toast.success("Disconnected from Bitbucket"); | ||
| } catch (e: any) { | ||
| toast.error(e?.message || "Failed to disconnect from Bitbucket"); | ||
| } finally { | ||
| busy = false; | ||
| } | ||
| } | ||
| </script> | ||
|
|
||
| <Card.Root> | ||
| <Card.CardHeader> | ||
| <div class="flex items-center justify-between gap-2"> | ||
| <Card.CardTitle>Bitbucket OAuth</Card.CardTitle> | ||
| {#if status} | ||
| <Badge.Root class={badgeColor}>{badgeLabel}</Badge.Root> | ||
| {/if} | ||
| </div> | ||
| </Card.CardHeader> | ||
| <Card.CardContent> | ||
| {#if loading} | ||
| <p class="text-muted-foreground text-sm">Loading...</p> | ||
| {:else if loadError} | ||
| <p class="text-destructive text-sm">{loadError}</p> | ||
| {:else if status} | ||
| <div class="flex flex-col gap-2"> | ||
| <p class="text-muted-foreground text-sm"> | ||
| {#if connected} | ||
| Webhook registration uses the connected Bitbucket app. | ||
| {:else} | ||
| Connect a Bitbucket app to manage repository webhooks. | ||
| {/if} | ||
| </p> | ||
| {#if connected} | ||
| <div class="flex flex-col gap-1"> | ||
| <span class="text-muted-foreground text-xs font-medium"> | ||
| Connected At | ||
| </span> | ||
| <span class="text-sm">{formatDate(status.connected_at)}</span> | ||
| </div> | ||
| <div class="flex flex-col gap-1"> | ||
| <span class="text-muted-foreground text-xs font-medium"> | ||
| Token Expires At | ||
| </span> | ||
| <span class="text-sm">{formatDate(status.expires_at)}</span> | ||
| </div> | ||
| {#if status.scopes?.length} | ||
| <div class="flex flex-col gap-1"> | ||
| <span class="text-muted-foreground text-xs font-medium"> | ||
| Scopes | ||
| </span> | ||
| <span class="text-sm">{status.scopes.join(", ")}</span> | ||
| </div> | ||
| {/if} | ||
| {/if} | ||
| </div> | ||
| {/if} | ||
| </Card.CardContent> | ||
| {#if !loading && (status || loadError)} | ||
| <Card.CardFooter class="justify-end gap-2"> | ||
| {#if loadError && !status} | ||
| <Button size="sm" variant="outline" onclick={loadStatus}>Retry</Button> | ||
| {/if} | ||
| {#if status} | ||
| {#if connected} | ||
| <Button | ||
| size="sm" | ||
| variant="destructive" | ||
| disabled={busy} | ||
| onclick={() => (showDisconnectDialog = true)} | ||
| > | ||
| Disconnect | ||
| </Button> | ||
| {:else} | ||
| <Button size="sm" disabled={busy} onclick={handleConnect}> | ||
| {busy ? "Connecting..." : "Connect with Bitbucket"} | ||
| </Button> | ||
| {/if} | ||
| {/if} | ||
| </Card.CardFooter> | ||
| {/if} | ||
| </Card.Root> | ||
|
|
||
| <Dialog.Root | ||
| bind:open={showDisconnectDialog} | ||
| title="Disconnect from Bitbucket?" | ||
| description="Remove the stored Bitbucket OAuth connection?" | ||
| > | ||
| {#snippet footer()} | ||
| <Button variant="ghost" onclick={() => (showDisconnectDialog = false)}> | ||
| Cancel | ||
| </Button> | ||
| <Button variant="destructive" onclick={handleDisconnect} disabled={busy}> | ||
| {busy ? "Disconnecting..." : "Disconnect"} | ||
| </Button> | ||
| {/snippet} | ||
| </Dialog.Root> |
Uh oh!
There was an error while loading. Please reload this page.