diff --git a/apps/backend/prisma/migrations/20250723001607_twitch/migration.sql b/apps/backend/prisma/migrations/20250723001607_twitch/migration.sql new file mode 100644 index 000000000..6451a8e0d --- /dev/null +++ b/apps/backend/prisma/migrations/20250723001607_twitch/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "StandardOAuthProviderType" ADD VALUE 'TWITCH'; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 60a1851b5..9b04c8400 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -363,6 +363,7 @@ enum StandardOAuthProviderType { LINKEDIN APPLE X + TWITCH } model OAuthToken { diff --git a/apps/backend/src/oauth/index.tsx b/apps/backend/src/oauth/index.tsx index f8a48056b..20d14f094 100644 --- a/apps/backend/src/oauth/index.tsx +++ b/apps/backend/src/oauth/index.tsx @@ -17,6 +17,7 @@ import { MicrosoftProvider } from "./providers/microsoft"; import { MockProvider } from "./providers/mock"; import { SpotifyProvider } from "./providers/spotify"; import { XProvider } from "./providers/x"; +import { TwitchProvider } from "./providers/twitch"; const _providers = { github: GithubProvider, @@ -30,6 +31,7 @@ const _providers = { bitbucket: BitbucketProvider, linkedin: LinkedInProvider, x: XProvider, + twitch: TwitchProvider, } as const; const mockProvider = MockProvider; diff --git a/apps/backend/src/oauth/providers/twitch.tsx b/apps/backend/src/oauth/providers/twitch.tsx new file mode 100644 index 000000000..f0241fa20 --- /dev/null +++ b/apps/backend/src/oauth/providers/twitch.tsx @@ -0,0 +1,56 @@ +import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; +import { OAuthUserInfo, validateUserInfo } from "../utils"; +import { OAuthBaseProvider, TokenSet } from "./base"; + +export class TwitchProvider extends OAuthBaseProvider { + private constructor( + ...args: ConstructorParameters + ) { + super(...args); + } + + static async create(options: { + clientId: string, + clientSecret: string, + }) { + return new TwitchProvider(...await OAuthBaseProvider.createConstructorArgs({ + issuer: "https://id.twitch.tv", + authorizationEndpoint: "https://id.twitch.tv/oauth2/authorize", + tokenEndpoint: "https://id.twitch.tv/oauth2/token", + tokenEndpointAuthMethod: "client_secret_post", + redirectUri: getEnvVariable("NEXT_PUBLIC_STACK_API_URL") + "/api/v1/auth/oauth/callback/twitch", + baseScope: "user:read:email", + ...options, + })); + } + + async postProcessUserInfo(tokenSet: TokenSet): Promise { + const info = await fetch("https://api.twitch.tv/helix/users", { + headers: { + Authorization: `Bearer ${tokenSet.accessToken}`, + "Client-Id": this.oauthClient.client_id as string, + }, + }).then((res) => res.json()); + + + const userInfo = info.data?.[0]; + + return validateUserInfo({ + accountId: userInfo.id, + displayName: userInfo.display_name, + email: userInfo.email, + profileImageUrl: userInfo.profile_image_url, + emailVerified: true, + }); + } + + async checkAccessTokenValidity(accessToken: string): Promise { + const info = await fetch("https://api.twitch.tv/helix/users", { + headers: { + Authorization: `Bearer ${accessToken}`, + "Client-Id": this.oauthClient.client_id as string, + }, + }).then((res) => res.json()); + return info.data?.[0] !== undefined; + } +} 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 71bd9bd08..958ad7abe 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 @@ -13,7 +13,7 @@ import * as yup from "yup"; export function ProviderIcon(props: { id: string }) { return (
@@ -40,6 +40,7 @@ function toTitle(id: string) { apple: "Apple", bitbucket: "Bitbucket", linkedin: "LinkedIn", + twitch: "Twitch", x: "X", }[id]; } @@ -216,7 +217,7 @@ export function ProviderSettingSwitch(props: Props) { return ( <>
{ if (enabled) { setTurnOffProviderDialogOpen(true); diff --git a/docs/src/components/layouts/docs-header-wrapper.tsx b/docs/src/components/layouts/docs-header-wrapper.tsx index c33964b44..f924d2e40 100644 --- a/docs/src/components/layouts/docs-header-wrapper.tsx +++ b/docs/src/components/layouts/docs-header-wrapper.tsx @@ -159,7 +159,7 @@ function MobileClickableCollapsibleSection({ e.stopPropagation(); setIsOpen(!isOpen); }} - className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded hover:bg-fd-muted/30" + className="transition-opacity p-0.5 rounded hover:bg-fd-muted/30" > {isOpen ? ( diff --git a/docs/src/components/layouts/docs.tsx b/docs/src/components/layouts/docs.tsx index b489055e5..7a0ae5d26 100644 --- a/docs/src/components/layouts/docs.tsx +++ b/docs/src/components/layouts/docs.tsx @@ -37,13 +37,13 @@ import Link from 'fumadocs-core/link'; import type { PageTree } from 'fumadocs-core/server'; import { NavProvider, - type PageStyles, StylesProvider, + type PageStyles, } from 'fumadocs-ui/contexts/layout'; import { TreeContextProvider } from 'fumadocs-ui/contexts/tree'; import { ArrowLeft, ChevronDown, ChevronRight, Languages, Sidebar as SidebarIcon } from 'lucide-react'; import { usePathname, useRouter } from 'next/navigation'; -import { createContext, type HTMLAttributes, type ReactNode, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { createContext, useContext, useEffect, useMemo, useRef, useState, type HTMLAttributes, type ReactNode } from 'react'; import { createPortal } from 'react-dom'; import { usePlatformPreference } from '../../hooks/use-platform-preference'; import { cn } from '../../lib/cn'; @@ -79,7 +79,7 @@ import { type IconItemType, type LinkItemType, } from './links'; -import { type BaseLayoutProps, getLinks, omit, slot, slots } from './shared'; +import { getLinks, omit, slot, slots, type BaseLayoutProps } from './shared'; import { isInApiSection } from './shared-header'; import { useSidebar as useCustomSidebar } from './sidebar-context'; @@ -245,7 +245,7 @@ function ClickableCollapsibleSection({ e.stopPropagation(); setIsOpen(!isOpen); }} - className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded hover:bg-fd-muted/30" + className="transition-opacity p-0.5 rounded hover:bg-fd-muted/30" > {isOpen ? ( diff --git a/docs/templates/concepts/auth-providers/apple.mdx b/docs/templates/concepts/auth-providers/apple.mdx index 34c0e678f..eecb514b9 100644 --- a/docs/templates/concepts/auth-providers/apple.mdx +++ b/docs/templates/concepts/auth-providers/apple.mdx @@ -1,5 +1,5 @@ --- -title: "Apple Authentication" +title: "Apple" --- This guide explains how to set up Apple as an authentication provider with Stack Auth. Sign in with Apple allows users to sign in to your application using their Apple ID. diff --git a/docs/templates/concepts/auth-providers/bitbucket.mdx b/docs/templates/concepts/auth-providers/bitbucket.mdx index 2941efc8e..ff3624c34 100644 --- a/docs/templates/concepts/auth-providers/bitbucket.mdx +++ b/docs/templates/concepts/auth-providers/bitbucket.mdx @@ -1,5 +1,5 @@ --- -title: "Bitbucket Authentication" +title: "Bitbucket" --- This guide explains how to set up Bitbucket as an authentication provider with Stack Auth. Bitbucket OAuth allows users to sign in to your application using their Bitbucket account. diff --git a/docs/templates/concepts/auth-providers/discord.mdx b/docs/templates/concepts/auth-providers/discord.mdx index be1feebd4..dd08e4825 100644 --- a/docs/templates/concepts/auth-providers/discord.mdx +++ b/docs/templates/concepts/auth-providers/discord.mdx @@ -1,5 +1,5 @@ --- -title: "Discord Authentication" +title: "Discord" --- This guide explains how to set up Discord as an authentication provider with Stack Auth. Discord OAuth2 allows users to sign in to your application using their Discord account. diff --git a/docs/templates/concepts/auth-providers/facebook.mdx b/docs/templates/concepts/auth-providers/facebook.mdx index fad6f8e8d..c266fd196 100644 --- a/docs/templates/concepts/auth-providers/facebook.mdx +++ b/docs/templates/concepts/auth-providers/facebook.mdx @@ -1,5 +1,5 @@ --- -title: "Facebook Authentication" +title: "Facebook" --- This guide explains how to set up Facebook as an authentication provider with Stack Auth. Facebook OAuth allows users to sign in to your application using their Facebook account. diff --git a/docs/templates/concepts/auth-providers/github.mdx b/docs/templates/concepts/auth-providers/github.mdx index f8297a714..8d0e60bba 100644 --- a/docs/templates/concepts/auth-providers/github.mdx +++ b/docs/templates/concepts/auth-providers/github.mdx @@ -1,5 +1,5 @@ --- -title: "GitHub Authentication" +title: "GitHub" --- This guide explains how to set up GitHub as an authentication provider with Stack Auth. GitHub OAuth allows users to sign in to your application using their GitHub account. diff --git a/docs/templates/concepts/auth-providers/gitlab.mdx b/docs/templates/concepts/auth-providers/gitlab.mdx index 4fa1a9671..6a75752d3 100644 --- a/docs/templates/concepts/auth-providers/gitlab.mdx +++ b/docs/templates/concepts/auth-providers/gitlab.mdx @@ -1,5 +1,5 @@ --- -title: "GitLab Authentication" +title: "GitLab" --- This guide explains how to set up GitLab as an authentication provider with Stack Auth. GitLab OAuth allows users to sign in to your application using their GitLab account. diff --git a/docs/templates/concepts/auth-providers/google.mdx b/docs/templates/concepts/auth-providers/google.mdx index 73454e051..783bedb4c 100644 --- a/docs/templates/concepts/auth-providers/google.mdx +++ b/docs/templates/concepts/auth-providers/google.mdx @@ -1,5 +1,5 @@ --- -title: "Google Authentication" +title: "Google" --- This guide explains how to set up Google as an authentication provider with Stack Auth. Google OAuth2 allows users to sign in to your application using their Google account. diff --git a/docs/templates/concepts/auth-providers/index.mdx b/docs/templates/concepts/auth-providers/index.mdx index bd27cc0db..c64776d9d 100644 --- a/docs/templates/concepts/auth-providers/index.mdx +++ b/docs/templates/concepts/auth-providers/index.mdx @@ -107,6 +107,27 @@ Stack Auth supports a wide range of authentication providers to help you add sec
+ + +
+ + + + + + + + + + + + +
+
## Other Authentication Methods diff --git a/docs/templates/concepts/auth-providers/linkedin.mdx b/docs/templates/concepts/auth-providers/linkedin.mdx index ede48051a..b6c224bd1 100644 --- a/docs/templates/concepts/auth-providers/linkedin.mdx +++ b/docs/templates/concepts/auth-providers/linkedin.mdx @@ -1,5 +1,5 @@ --- -title: "LinkedIn Authentication" +title: "LinkedIn" --- This guide explains how to set up LinkedIn as an authentication provider with Stack Auth. LinkedIn OAuth2 allows users to sign in to your application using their LinkedIn account. diff --git a/docs/templates/concepts/auth-providers/meta.json b/docs/templates/concepts/auth-providers/meta.json index 5ba2f421e..3c4043c3d 100644 --- a/docs/templates/concepts/auth-providers/meta.json +++ b/docs/templates/concepts/auth-providers/meta.json @@ -13,6 +13,7 @@ "bitbucket", "linkedin", "x-twitter", + "twitch", "passkey", "two-factor-auth" ] diff --git a/docs/templates/concepts/auth-providers/microsoft.mdx b/docs/templates/concepts/auth-providers/microsoft.mdx index c77ab9f35..4972c6387 100644 --- a/docs/templates/concepts/auth-providers/microsoft.mdx +++ b/docs/templates/concepts/auth-providers/microsoft.mdx @@ -1,5 +1,5 @@ --- -title: "Microsoft Authentication" +title: "Microsoft" --- This guide explains how to set up Microsoft as an authentication provider with Stack Auth. Microsoft OAuth allows users to sign in to your application using their Microsoft account. diff --git a/docs/templates/concepts/auth-providers/passkey.mdx b/docs/templates/concepts/auth-providers/passkey.mdx index f9a8ee884..937646e06 100644 --- a/docs/templates/concepts/auth-providers/passkey.mdx +++ b/docs/templates/concepts/auth-providers/passkey.mdx @@ -1,5 +1,5 @@ --- -title: "Passkey Authentication" +title: "Passkey" --- This guide explains how to set up Passkey authentication with Stack Auth. Passkeys allow users to sign in to your application securely using biometrics, mobile devices, or security keys. diff --git a/docs/templates/concepts/auth-providers/spotify.mdx b/docs/templates/concepts/auth-providers/spotify.mdx index 83e32e97d..8529dcc85 100644 --- a/docs/templates/concepts/auth-providers/spotify.mdx +++ b/docs/templates/concepts/auth-providers/spotify.mdx @@ -1,5 +1,5 @@ --- -title: "Spotify Authentication" +title: "Spotify" --- This guide explains how to set up Spotify as an authentication provider with Stack Auth. Spotify OAuth allows users to sign in to your application using their Spotify account. diff --git a/docs/templates/concepts/auth-providers/twitch.mdx b/docs/templates/concepts/auth-providers/twitch.mdx new file mode 100644 index 000000000..f91d8ff11 --- /dev/null +++ b/docs/templates/concepts/auth-providers/twitch.mdx @@ -0,0 +1,36 @@ +--- +title: "Twitch" +--- + +This guide explains how to set up Twitch as an authentication provider with Stack Auth. Twitch OAuth allows users to sign in to your application using their Twitch account. + +## Integration Steps + + + + ### Create a Twitch OAuth App + + 1. Navigate to the [Twitch Developer Console](https://dev.twitch.tv/console). + 2. Log in with your Twitch account. + 3. Navigate to **Applications** and click **Register New Application**. + 4. Enter a **Name** and select a **Category**. + 5. Under **OAuth Redirect URLs**, add `https://api.stack-auth.com/api/v1/auth/oauth/callback/twitch` + 6. Click **Create**. + 7. You'll be redirected to your app's dashboard. + 8. Click **Manage** of the app you just created to view more details about your app. + 9. Click "New Secret" to generate a new secret. + 10. Copy and save the **Client ID** and **Client Secret**. + + + ### Enable Twitch OAuth in Stack Auth + + 1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar. + 2. Click **Add SSO Providers** and select **Twitch** as the provider. + 3. Set the **Client ID** and **Client Secret** you obtained from the Twitch Developer Console earlier. + + + +### Need More Help? + +- Check the [Twitch OAuth Documentation](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/) +- Join our [Discord](https://discord.stack-auth.com) diff --git a/docs/templates/concepts/auth-providers/x-twitter.mdx b/docs/templates/concepts/auth-providers/x-twitter.mdx index d1b501a35..a583febbc 100644 --- a/docs/templates/concepts/auth-providers/x-twitter.mdx +++ b/docs/templates/concepts/auth-providers/x-twitter.mdx @@ -1,5 +1,5 @@ --- -title: "X (Twitter) Authentication" +title: "X (Twitter)" --- This guide explains how to set up X (formerly Twitter) as an authentication provider with Stack Auth. X OAuth 2.0 allows users to sign in to your application using their X account. diff --git a/packages/stack-shared/src/utils/oauth.tsx b/packages/stack-shared/src/utils/oauth.tsx index df422db93..e8972bfe2 100644 --- a/packages/stack-shared/src/utils/oauth.tsx +++ b/packages/stack-shared/src/utils/oauth.tsx @@ -1,4 +1,4 @@ -export const standardProviders = ["google", "github", "microsoft", "spotify", "facebook", "discord", "gitlab", "bitbucket", "linkedin", "apple", "x"] as const; +export const standardProviders = ["google", "github", "microsoft", "spotify", "facebook", "discord", "gitlab", "bitbucket", "linkedin", "apple", "x", "twitch"] as const; // No more shared providers should be added except for special cases export const sharedProviders = ["google", "github", "microsoft", "spotify"] as const; export const allProviders = standardProviders; diff --git a/packages/stack-ui/src/components/brand-icons.tsx b/packages/stack-ui/src/components/brand-icons.tsx index a0d5b7989..e02b2ecf1 100644 --- a/packages/stack-ui/src/components/brand-icons.tsx +++ b/packages/stack-ui/src/components/brand-icons.tsx @@ -1,62 +1,70 @@ import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; -export function Google({ iconSize } : { iconSize: number} ) { +export function Google({ iconSize }: { iconSize: number }) { return ( - - - - - + + + + + ); } -export function Facebook({ iconSize } : { iconSize: number} ) { +export function Facebook({ iconSize }: { iconSize: number }) { return ( - + ); } -export function GitHub({ iconSize } : { iconSize: number} ) { +export function GitHub({ iconSize }: { iconSize: number }) { return ( - + ); } -export function Microsoft({ iconSize } : { iconSize: number} ) { +export function Microsoft({ iconSize }: { iconSize: number }) { return ( {"MS-SymbolLockup"} - - - - + + + + ); } -export function Spotify({ iconSize } : { iconSize: number} ) { +export function Spotify({ iconSize }: { iconSize: number }) { return ( - + ); } -export function Discord({ iconSize } : { iconSize: number} ) { +export function Discord({ iconSize }: { iconSize: number }) { return ( - + ); } -export function Gitlab({ iconSize } : { iconSize: number} ) { +export function Gitlab({ iconSize }: { iconSize: number }) { return ( - - - - - - - + + + + + + + ); @@ -95,19 +117,23 @@ export function Bitbucket({ iconSize }: { iconSize: number }) { y1="13.818%" y2="78.776%" > - - + + - - - + + + ); } -export function LinkedIn({ iconSize } : { iconSize: number} ) { +export function LinkedIn({ iconSize }: { iconSize: number }) { return ( - - - + + + ); } -export function Apple({ iconSize } : { iconSize: number} ) { +export function Apple({ iconSize }: { iconSize: number }) { return ( - - - + + + ); } -export function X({ iconSize } : { iconSize: number} ) { +export function X({ iconSize }: { iconSize: number }) { return ( - - + + + + ); +} + +export function Twitch({ iconSize }: { iconSize: number }) { + return ( + + + + + + + + + + + ); } @@ -146,42 +199,45 @@ export function Mapping({ provider, iconSize, }: { - provider: string, - iconSize: number, + provider: string, + iconSize: number, }) { switch (provider) { case "google": { - return ; + return ; } case "github": { - return ; + return ; } case "facebook": { - return ; + return ; } case "microsoft": { - return ; + return ; } case "spotify": { - return ; + return ; } case "discord": { - return ; + return ; } case "gitlab": { - return ; + return ; } case "bitbucket": { - return ; + return ; } case "linkedin": { - return ; + return ; } case "apple": { - return ; + return ; } case "x": { - return ; + return ; + } + case "twitch": { + return ; } default: { throw new StackAssertionError(`Icon not found for provider: ${provider}`); @@ -202,6 +258,7 @@ export function toTitle(id: string) { bitbucket: "Bitbucket", linkedin: "LinkedIn", x: "X", + twitch: "Twitch" }[id] || throwErr(`Unknown provider: ${id}`); } @@ -215,4 +272,5 @@ export const BRAND_COLORS: Record = { linkedin: '#0A66C2', x: '#000000', apple: '#000000', + twitch: '#ffffff' }; diff --git a/packages/template/src/components/oauth-button.tsx b/packages/template/src/components/oauth-button.tsx index 52bde4147..f7a964385 100644 --- a/packages/template/src/components/oauth-button.tsx +++ b/packages/template/src/components/oauth-button.tsx @@ -147,6 +147,15 @@ export function OAuthButton({ }; break; } + case 'twitch': { + style = { + backgroundColor: "#6441a5", + textColor: "#fff", + name: "Twitch", + icon: , + }; + break; + } default: { style = { name: provider,