Skip to content

Commit 6a58bb0

Browse files
Nachwahlfomalhautb
andauthored
Add an OAuth provider for Twitch (#728)
<!-- ELLIPSIS_HIDDEN --> > [!IMPORTANT] > Add Twitch as a new OAuth provider, updating backend logic and UI components to support Twitch authentication. > > - **Behavior**: > - Add `TwitchProvider` class in `providers/twitch.tsx` to handle OAuth with Twitch, including user info post-processing. > - Update `_providers` in `index.tsx` to include `TwitchProvider`. > - Add `TWITCH` to `StandardOAuthProviderType` enum in `schema.prisma`. > - **UI Components**: > - Add Twitch icon and color in `brand-icons.tsx` and `BRAND_COLORS`. > - Update `ProviderIcon`, `ProviderSettingDialog`, and `OAuthButton` to support Twitch in `providers.tsx` and `oauth-button.tsx`. > - **Misc**: > - Add `twitch` to `standardProviders` in `oauth.tsx`. > > <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup> for 08c0de5. You can [customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this summary. It will automatically update as commits are pushed.</sup> <!-- ELLIPSIS_HIDDEN --> --------- Co-authored-by: Zai Shi <zaishi00@outlook.com>
1 parent fc78504 commit 6a58bb0

25 files changed

Lines changed: 263 additions & 76 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterEnum
2+
ALTER TYPE "StandardOAuthProviderType" ADD VALUE 'TWITCH';

apps/backend/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ enum StandardOAuthProviderType {
363363
LINKEDIN
364364
APPLE
365365
X
366+
TWITCH
366367
}
367368

368369
model OAuthToken {

apps/backend/src/oauth/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { MicrosoftProvider } from "./providers/microsoft";
1717
import { MockProvider } from "./providers/mock";
1818
import { SpotifyProvider } from "./providers/spotify";
1919
import { XProvider } from "./providers/x";
20+
import { TwitchProvider } from "./providers/twitch";
2021

2122
const _providers = {
2223
github: GithubProvider,
@@ -30,6 +31,7 @@ const _providers = {
3031
bitbucket: BitbucketProvider,
3132
linkedin: LinkedInProvider,
3233
x: XProvider,
34+
twitch: TwitchProvider,
3335
} as const;
3436

3537
const mockProvider = MockProvider;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
2+
import { OAuthUserInfo, validateUserInfo } from "../utils";
3+
import { OAuthBaseProvider, TokenSet } from "./base";
4+
5+
export class TwitchProvider extends OAuthBaseProvider {
6+
private constructor(
7+
...args: ConstructorParameters<typeof OAuthBaseProvider>
8+
) {
9+
super(...args);
10+
}
11+
12+
static async create(options: {
13+
clientId: string,
14+
clientSecret: string,
15+
}) {
16+
return new TwitchProvider(...await OAuthBaseProvider.createConstructorArgs({
17+
issuer: "https://id.twitch.tv",
18+
authorizationEndpoint: "https://id.twitch.tv/oauth2/authorize",
19+
tokenEndpoint: "https://id.twitch.tv/oauth2/token",
20+
tokenEndpointAuthMethod: "client_secret_post",
21+
redirectUri: getEnvVariable("NEXT_PUBLIC_STACK_API_URL") + "/api/v1/auth/oauth/callback/twitch",
22+
baseScope: "user:read:email",
23+
...options,
24+
}));
25+
}
26+
27+
async postProcessUserInfo(tokenSet: TokenSet): Promise<OAuthUserInfo> {
28+
const info = await fetch("https://api.twitch.tv/helix/users", {
29+
headers: {
30+
Authorization: `Bearer ${tokenSet.accessToken}`,
31+
"Client-Id": this.oauthClient.client_id as string,
32+
},
33+
}).then((res) => res.json());
34+
35+
36+
const userInfo = info.data?.[0];
37+
38+
return validateUserInfo({
39+
accountId: userInfo.id,
40+
displayName: userInfo.display_name,
41+
email: userInfo.email,
42+
profileImageUrl: userInfo.profile_image_url,
43+
emailVerified: true,
44+
});
45+
}
46+
47+
async checkAccessTokenValidity(accessToken: string): Promise<boolean> {
48+
const info = await fetch("https://api.twitch.tv/helix/users", {
49+
headers: {
50+
Authorization: `Bearer ${accessToken}`,
51+
"Client-Id": this.oauthClient.client_id as string,
52+
},
53+
}).then((res) => res.json());
54+
return info.data?.[0] !== undefined;
55+
}
56+
}

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import * as yup from "yup";
1313
export function ProviderIcon(props: { id: string }) {
1414
return (
1515
<div
16-
className="flex items-center justify-center w-12 h-12 rounded-md border border-gray-800"
16+
className="flex items-center justify-center w-12 h-12 rounded-md border"
1717
style={{ backgroundColor: props.id in BrandIcons.BRAND_COLORS ? BrandIcons.BRAND_COLORS[props.id] : undefined }}
1818
>
1919
<BrandIcons.Mapping iconSize={24} provider={props.id} />
@@ -40,6 +40,7 @@ function toTitle(id: string) {
4040
apple: "Apple",
4141
bitbucket: "Bitbucket",
4242
linkedin: "LinkedIn",
43+
twitch: "Twitch",
4344
x: "X",
4445
}[id];
4546
}
@@ -216,7 +217,7 @@ export function ProviderSettingSwitch(props: Props) {
216217
return (
217218
<>
218219
<div
219-
className={clsx("flex flex-col items-center justify-center gap-2 py-2 border border-1 rounded-lg p-2 w-[120px] h-[120px] cursor-pointer transition-all", enabled ? "border-white" : "border-gray-800 hover:border-gray-400")}
220+
className={clsx("flex flex-col items-center justify-center gap-2 py-2 border border-1 rounded-lg p-2 w-[120px] h-[120px] transition-all hover:border-gray-500 cursor-pointer")}
220221
onClick={() => {
221222
if (enabled) {
222223
setTurnOffProviderDialogOpen(true);

docs/src/components/layouts/docs-header-wrapper.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ function MobileClickableCollapsibleSection({
159159
e.stopPropagation();
160160
setIsOpen(!isOpen);
161161
}}
162-
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded hover:bg-fd-muted/30"
162+
className="transition-opacity p-0.5 rounded hover:bg-fd-muted/30"
163163
>
164164
{isOpen ? (
165165
<ChevronDown className="h-3 w-3" />

docs/src/components/layouts/docs.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ import Link from 'fumadocs-core/link';
3737
import type { PageTree } from 'fumadocs-core/server';
3838
import {
3939
NavProvider,
40-
type PageStyles,
4140
StylesProvider,
41+
type PageStyles,
4242
} from 'fumadocs-ui/contexts/layout';
4343
import { TreeContextProvider } from 'fumadocs-ui/contexts/tree';
4444
import { ArrowLeft, ChevronDown, ChevronRight, Languages, Sidebar as SidebarIcon } from 'lucide-react';
4545
import { usePathname, useRouter } from 'next/navigation';
46-
import { createContext, type HTMLAttributes, type ReactNode, useContext, useEffect, useMemo, useRef, useState } from 'react';
46+
import { createContext, useContext, useEffect, useMemo, useRef, useState, type HTMLAttributes, type ReactNode } from 'react';
4747
import { createPortal } from 'react-dom';
4848
import { usePlatformPreference } from '../../hooks/use-platform-preference';
4949
import { cn } from '../../lib/cn';
@@ -79,7 +79,7 @@ import {
7979
type IconItemType,
8080
type LinkItemType,
8181
} from './links';
82-
import { type BaseLayoutProps, getLinks, omit, slot, slots } from './shared';
82+
import { getLinks, omit, slot, slots, type BaseLayoutProps } from './shared';
8383
import { isInApiSection } from './shared-header';
8484
import { useSidebar as useCustomSidebar } from './sidebar-context';
8585

@@ -245,7 +245,7 @@ function ClickableCollapsibleSection({
245245
e.stopPropagation();
246246
setIsOpen(!isOpen);
247247
}}
248-
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded hover:bg-fd-muted/30"
248+
className="transition-opacity p-0.5 rounded hover:bg-fd-muted/30"
249249
>
250250
{isOpen ? (
251251
<ChevronDown className="h-3 w-3" />

docs/templates/concepts/auth-providers/apple.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: "Apple Authentication"
2+
title: "Apple"
33
---
44

55
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.

docs/templates/concepts/auth-providers/bitbucket.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: "Bitbucket Authentication"
2+
title: "Bitbucket"
33
---
44

55
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.

docs/templates/concepts/auth-providers/discord.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: "Discord Authentication"
2+
title: "Discord"
33
---
44

55
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.

0 commit comments

Comments
 (0)