diff --git a/apps/backend/prisma/migrations/20240904155848_add_bitbucket_oauth/migration.sql b/apps/backend/prisma/migrations/20240904155848_add_bitbucket_oauth/migration.sql new file mode 100644 index 000000000..44605b477 --- /dev/null +++ b/apps/backend/prisma/migrations/20240904155848_add_bitbucket_oauth/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "StandardOAuthProviderType" ADD VALUE 'BITBUCKET'; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 81655092c..dfb68b061 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -581,6 +581,7 @@ enum StandardOAuthProviderType { SPOTIFY DISCORD GITLAB + BITBUCKET } //#endregion diff --git a/apps/backend/src/oauth/index.tsx b/apps/backend/src/oauth/index.tsx index c3403410c..2907f69c2 100644 --- a/apps/backend/src/oauth/index.tsx +++ b/apps/backend/src/oauth/index.tsx @@ -12,6 +12,7 @@ import { SpotifyProvider } from "./providers/spotify"; import { MockProvider } from "./providers/mock"; import { DiscordProvider } from "@/oauth/providers/discord"; import { GitlabProvider } from "./providers/gitlab"; +import { BitbucketProvider } from "./providers/bitbucket"; const _providers = { github: GithubProvider, @@ -21,6 +22,7 @@ const _providers = { spotify: SpotifyProvider, discord: DiscordProvider, gitlab: GitlabProvider, + bitbucket: BitbucketProvider, } as const; const mockProvider = MockProvider; diff --git a/apps/backend/src/oauth/providers/bitbucket.tsx b/apps/backend/src/oauth/providers/bitbucket.tsx new file mode 100644 index 000000000..9e21a92af --- /dev/null +++ b/apps/backend/src/oauth/providers/bitbucket.tsx @@ -0,0 +1,48 @@ +import { OAuthBaseProvider, TokenSet } from "./base"; +import { OAuthUserInfo, validateUserInfo } from "../utils"; +import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; + +export class BitbucketProvider extends OAuthBaseProvider { + private constructor( + ...args: ConstructorParameters + ) { + super(...args); + } + + static async create(options: { clientId: string, clientSecret: string }) { + return new BitbucketProvider( + ...(await OAuthBaseProvider.createConstructorArgs({ + issuer: "https://bitbucket.org", + authorizationEndpoint: "https://bitbucket.org/site/oauth2/authorize", + tokenEndpoint: "https://bitbucket.org/site/oauth2/access_token", + redirectUri: + getEnvVariable("STACK_BASE_URL") + + "/api/v1/auth/oauth/callback/bitbucket", + baseScope: "account email", + ...options, + })) + ); + } + + async postProcessUserInfo(tokenSet: TokenSet): Promise { + const headers = { + Authorization: `Bearer ${tokenSet.accessToken}`, + }; + const [userInfo, emailData] = await Promise.all([ + fetch("https://api.bitbucket.org/2.0/user", { headers }).then((res) => + res.json() + ), + fetch("https://api.bitbucket.org/2.0/user/emails", { headers }).then( + (res) => res.json() + ), + ]); + + return validateUserInfo({ + accountId: userInfo.account_id, + displayName: userInfo.display_name, + email: emailData?.values[0].email, + profileImageUrl: userInfo.links.avatar.href, + emailVerified: emailData?.values[0].is_confirmed, + }); + } +} diff --git a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx index 68cb99534..67d36c2f4 100644 --- a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx @@ -11,7 +11,7 @@ import * as yup from "yup"; export const projectFormSchema = yup.object({ displayName: yup.string().min(1, "Display name is required").required(), - signInMethods: yup.array(yup.string().oneOf(["google", "github", "microsoft", "facebook", "credential", "magicLink", "discord", "gitlab"]).required()).required(), + signInMethods: yup.array(yup.string().oneOf(["google", "github", "microsoft", "facebook", "credential", "magicLink", "discord", "gitlab", "bitbucket"]).required()).required(), }); export type ProjectFormValues = yup.InferType diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx index db7c89c5a..e83754856 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx @@ -24,6 +24,7 @@ function toTitle(id: string) { spotify: "Spotify", discord: "Discord", gitlab: "GitLab", + bitbucket: "Bitbucket", }[id]; } diff --git a/apps/oauth-mock-server/src/index.ts b/apps/oauth-mock-server/src/index.ts index 304e2a9bc..34857ada5 100644 --- a/apps/oauth-mock-server/src/index.ts +++ b/apps/oauth-mock-server/src/index.ts @@ -10,6 +10,7 @@ const mockedProviders = [ "spotify", "discord", "gitlab", + "bitbucket", ]; const configuration: Configuration = { diff --git a/docs/fern/docs/pages/getting-started/production.mdx b/docs/fern/docs/pages/getting-started/production.mdx index 1121e9cf2..c0b0a70f0 100644 --- a/docs/fern/docs/pages/getting-started/production.mdx +++ b/docs/fern/docs/pages/getting-started/production.mdx @@ -53,6 +53,11 @@ To use your own OAuth provider setups in production, follow these steps for each Callback URL: `https://api.stack-auth.com/api/v1/auth/oauth/callback/gitlab` + + [Bitbucket OAuth Setup Guide](https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud) + Callback URL: + `https://api.stack-auth.com/api/v1/auth/oauth/callback/bitbucket` + 2. **Enter OAuth Credentials**: Go to the `Auth Methods` section in the Stack dashboard, open the provider's settings, switch from shared keys to custom keys, and enter the client ID and client secret. diff --git a/packages/stack-shared/src/utils/oauth.tsx b/packages/stack-shared/src/utils/oauth.tsx index 66a3709a5..077d72a2b 100644 --- a/packages/stack-shared/src/utils/oauth.tsx +++ b/packages/stack-shared/src/utils/oauth.tsx @@ -1,7 +1,7 @@ -export const standardProviders = ["google", "github", "facebook", "microsoft", "spotify", "discord", "gitlab"] as const; +export const standardProviders = ["google", "github", "facebook", "microsoft", "spotify", "discord", "gitlab", "bitbucket"] as const; // No more shared providers should be added except for special cases export const sharedProviders = ["google", "github", "facebook", "microsoft", "spotify"] as const; -export const allProviders = ["google", "github", "facebook", "microsoft", "spotify", "discord", "gitlab"] as const; +export const allProviders = ["google", "github", "facebook", "microsoft", "spotify", "discord", "gitlab", "bitbucket"] as const; export type ProviderType = typeof allProviders[number]; export type StandardProviderType = typeof standardProviders[number]; diff --git a/packages/stack/src/components/oauth-button.tsx b/packages/stack/src/components/oauth-button.tsx index 92517aea5..0fccd664d 100644 --- a/packages/stack/src/components/oauth-button.tsx +++ b/packages/stack/src/components/oauth-button.tsx @@ -72,39 +72,45 @@ function GitlabIcon({ iconSize } : { iconSize: number} ) { preserveAspectRatio="xMidYMid" > - - - - - - - + + + + + + + ); } +function BitbucketIcon({ iconSize }: { iconSize: number }) { + return ( + + + + + + + + + + + + ); +} const changeColor = (c: Color, value: number) => { if (c.isLight()) { @@ -197,6 +203,16 @@ export function OAuthButton({ }; break; } + case "bitbucket": { + style = { + backgroundColor: "#fff", + textColor: "#000", + border: "1px solid #ddd", + name: "Bitbucket", + icon: , + }; + break; + } default: { style = { name: provider,