From 330dc6fa0578a8ff579b7f8971db1ecb651a681a Mon Sep 17 00:00:00 2001 From: Samuel Jensen <44519206+nichtsam@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:44:32 +0100 Subject: [PATCH] bring back discord auth --- app/utils/auth/connections.server.ts | 2 + app/utils/auth/connections.tsx | 10 +- app/utils/auth/providers/discord.server.ts | 109 +++++++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 17 ++++ 5 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 app/utils/auth/providers/discord.server.ts diff --git a/app/utils/auth/connections.server.ts b/app/utils/auth/connections.server.ts index 3a89ac1..077004c 100644 --- a/app/utils/auth/connections.server.ts +++ b/app/utils/auth/connections.server.ts @@ -3,6 +3,7 @@ import { Authenticator } from 'remix-auth' import { env } from '#app/utils/env.server.ts' import { type ServerTiming } from '../timings.server.ts' import { type ProviderName } from './connections.tsx' +import { DiscordProvider } from './providers/discord.server.ts' import { GitHubProvider } from './providers/github.server.ts' import { type AuthProvider, type ProviderUser } from './providers/model.ts' @@ -19,6 +20,7 @@ export const connectionSessionStorage = createCookieSessionStorage({ export const providers: Record = { github: new GitHubProvider(), + discord: new DiscordProvider(), } export const createAuthenticator = (request: Request) => { diff --git a/app/utils/auth/connections.tsx b/app/utils/auth/connections.tsx index d5a4647..a32d2c3 100644 --- a/app/utils/auth/connections.tsx +++ b/app/utils/auth/connections.tsx @@ -5,8 +5,12 @@ import { Icon } from '#app/components/ui/icon.tsx' import { useIsPending } from '../ui.ts' export const GITHUB_PROVIDER_NAME = 'github' +export const DISCORD_PROVIDER_NAME = 'discord' -export const providerNames = [GITHUB_PROVIDER_NAME] as const +export const providerNames = [ + GITHUB_PROVIDER_NAME, + DISCORD_PROVIDER_NAME, +] as const export const ProviderNameSchema = z.enum(providerNames) export type ProviderName = z.infer @@ -19,6 +23,10 @@ export const providerConfigs: Record = { label: 'Github', icon: , }, + [DISCORD_PROVIDER_NAME]: { + label: 'Discord', + icon: , + }, } export const ProviderConnectionForm = ({ diff --git a/app/utils/auth/providers/discord.server.ts b/app/utils/auth/providers/discord.server.ts new file mode 100644 index 0000000..8462fd4 --- /dev/null +++ b/app/utils/auth/providers/discord.server.ts @@ -0,0 +1,109 @@ +import { DiscordStrategy } from '@nichtsam/remix-auth-discord' +import { z } from 'zod' +import { cachified, longLivedCache } from '#app/utils/cache.server.ts' +import { env } from '#app/utils/env.server.ts' +import { type ServerTiming } from '#app/utils/timings.server.ts' +import { type AuthProvider } from './model.ts' + +const getEmail = (profile: Profile) => + profile.verified && profile.email ? profile.email : undefined +const getDisplayName = (profile: Profile) => + profile.global_name ?? profile.username +const getAvatarUrl = (profile: Profile) => + profile.avatar + ? `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png` + : undefined + +// pick needed from here: https://discord.com/developers/docs/resources/user#user-object +type Profile = z.infer +const ProfileSchema = z.object({ + id: z.string(), + username: z.string(), + global_name: z.string().nullable(), + email: z.string().nullable().optional(), + verified: z.boolean().optional(), + avatar: z.string().nullable(), +}) +const getProfile = async (accessToken: string) => { + const response = await fetch('https://discord.com/api/v10/users/@me', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + const result = ProfileSchema.safeParse(await response.json()) + if (!result.success) { + throw new Error( + 'Corrupted discord profile, make sure the schema is in sync with lastet github api schema', + ) + } + return result.data +} + +export class DiscordProvider implements AuthProvider { + getAuthStrategy(request: Request) { + return new DiscordStrategy( + { + clientId: env.DISCORD_CLIENT_ID, + clientSecret: env.DISCORD_CLIENT_SECRET, + redirectURI: new URL('/auth/discord/callback', request.url), + scopes: ['identify', 'email'], + }, + async ({ tokens }) => { + const accessToken = tokens.accessToken() + const profile = await getProfile(accessToken) + + return { + id: profile.id, + email: getEmail(profile), + username: profile.username, + name: getDisplayName(profile), + imageUrl: getAvatarUrl(profile), + } + }, + ) + } + + async resolveConnectionInfo( + providerId: string, + { timing }: { timing?: ServerTiming } = {}, + ) { + const result = await cachified({ + key: `connection-info:discord:${providerId}`, + cache: longLivedCache, + ttl: 1000 * 60, + swr: 1000 * 60 * 60 * 24 * 7, + timing, + getFreshValue: async (context) => { + const response = await fetch( + `https://discord.com/api/v10/users/${providerId}`, + { + headers: { + Authorization: `Bot ${env.DISCORD_BOT_TOKEN}`, + }, + }, + ) + const rawJson = await response.json() + const result = ProfileSchema.safeParse(rawJson) + + if (!result.success) { + context.metadata.ttl = 0 + } + + return result + }, + }) + + if (!result.success) { + return { + connectionUserDisplayName: 'Unknown' as const, + profileLink: null, + } + } + + return { + connectionUserDisplayName: getDisplayName(result.data), + profileLink: null, + } + } +} diff --git a/package.json b/package.json index 709f1ef..ff4a4e2 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@epic-web/remember": "1.1.0", "@libsql/client": "0.14.0", "@nasa-gcn/remix-seo": "2.0.1", + "@nichtsam/remix-auth-discord": "3.0.0", "@radix-ui/react-aspect-ratio": "1.1.1", "@radix-ui/react-avatar": "1.1.2", "@radix-ui/react-checkbox": "1.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7971991..f9738e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@nasa-gcn/remix-seo': specifier: 2.0.1 version: 2.0.1(@remix-run/react@2.15.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2))(@remix-run/server-runtime@2.15.2(typescript@5.7.2)) + '@nichtsam/remix-auth-discord': + specifier: 3.0.0 + version: 3.0.0(remix-auth@4.1.0) '@radix-ui/react-aspect-ratio': specifier: 1.1.1 version: 1.1.1(@types/react-dom@19.0.2(@types/react@19.0.2))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1407,6 +1410,11 @@ packages: '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + '@nichtsam/remix-auth-discord@3.0.0': + resolution: {integrity: sha512-Ex1D27v3Z8pDdSG1rBTsu6QEuQMksi06T6B73+tBdYTj87Y5UWmFZuY8NAhESgTcXpGUHgG5iuZSKQc9gcrwDw==} + peerDependencies: + remix-auth: ^4.0.0 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -7899,6 +7907,15 @@ snapshots: '@neon-rs/load@0.0.4': {} + '@nichtsam/remix-auth-discord@3.0.0(remix-auth@4.1.0)': + dependencies: + '@mjackson/headers': 0.9.0 + arctic: 3.1.2 + debug: 4.3.7 + remix-auth: 4.1.0 + transitivePeerDependencies: + - supports-color + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5