From 3611e238698e8ebc5d003c9f3786b696d56970e2 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:41:03 +0100 Subject: [PATCH] feat: migrate to idv --- apps/web/src/app/(ui)/(protected)/layout.tsx | 2 +- .../settings/channel/[channelName]/page.tsx | 2 +- .../{slack => hackclub}/callback/route.ts | 79 ++++++++----------- .../auth/{slack => hackclub}/route.ts | 6 +- apps/web/src/components/app/NavBar/NavBar.tsx | 2 +- packages/auth/src/index.ts | 14 +++- .../20251124212814_add_emails/migration.sql | 2 + packages/db/prisma/schema.prisma | 1 + slack-import-emojis/src/main.rs | 36 ++++----- 9 files changed, 72 insertions(+), 72 deletions(-) rename apps/web/src/app/(ui)/(public)/auth/{slack => hackclub}/callback/route.ts (53%) rename apps/web/src/app/(ui)/(public)/auth/{slack => hackclub}/route.ts (58%) create mode 100644 packages/db/prisma/migrations/20251124212814_add_emails/migration.sql diff --git a/apps/web/src/app/(ui)/(protected)/layout.tsx b/apps/web/src/app/(ui)/(protected)/layout.tsx index 63212e5..1b5f393 100644 --- a/apps/web/src/app/(ui)/(protected)/layout.tsx +++ b/apps/web/src/app/(ui)/(protected)/layout.tsx @@ -4,7 +4,7 @@ import { redirect, RedirectType } from 'next/navigation'; export default async function Layout({ children }: { children: React.ReactNode }) { const { user } = await validateRequest(); if (!user) { - return redirect('/auth/slack'); + return redirect('/auth/hackclub'); } if (!user.hasOnboarded) { return redirect(`/onboarding`, RedirectType.push); diff --git a/apps/web/src/app/(ui)/(protected)/settings/channel/[channelName]/page.tsx b/apps/web/src/app/(ui)/(protected)/settings/channel/[channelName]/page.tsx index e3e131c..e1c490e 100644 --- a/apps/web/src/app/(ui)/(protected)/settings/channel/[channelName]/page.tsx +++ b/apps/web/src/app/(ui)/(protected)/settings/channel/[channelName]/page.tsx @@ -13,7 +13,7 @@ export default async function ChannelSettingsPage({ const { user } = await validateRequest(); if (!user) { - redirect('/auth/slack'); + redirect('/auth/hackclub'); } const channel = await prisma.channel.findUnique({ diff --git a/apps/web/src/app/(ui)/(public)/auth/slack/callback/route.ts b/apps/web/src/app/(ui)/(public)/auth/hackclub/callback/route.ts similarity index 53% rename from apps/web/src/app/(ui)/(public)/auth/slack/callback/route.ts rename to apps/web/src/app/(ui)/(public)/auth/hackclub/callback/route.ts index 4a6aef4..e0cd6b6 100644 --- a/apps/web/src/app/(ui)/(public)/auth/slack/callback/route.ts +++ b/apps/web/src/app/(ui)/(public)/auth/hackclub/callback/route.ts @@ -1,6 +1,6 @@ -import { slack, lucia } from '@hctv/auth'; +import { hackClub, lucia, HCID_TOKEN_URL, HCID_USER_INFO_URL } from '@hctv/auth'; import { cookies as nextCookies } from 'next/headers'; -import { decodeIdToken, OAuth2RequestError } from 'arctic'; +import { OAuth2RequestError } from 'arctic'; import { generateIdFromEntropySize } from 'lucia'; import { prisma } from '@hctv/db'; import { getRedisConnection } from '@hctv/db'; @@ -10,7 +10,7 @@ export async function GET(request: Request): Promise { const url = new URL(request.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); - const storedState = cookies.get("slack_oauth_state")?.value ?? null; + const storedState = cookies.get("hackclub_oauth_state")?.value ?? null; if (!code || !state || !storedState || state !== storedState) { console.log('invalid state stuff'); return new Response(null, { @@ -19,22 +19,33 @@ export async function GET(request: Request): Promise { } try { - const tokens = await slack.validateAuthorizationCode(code); - const accessToken = tokens.accessToken() - const slackUserResponse = await fetch('https://slack.com/api/openid.connect.userInfo', { + const tokens = await hackClub.validateAuthorizationCode(HCID_TOKEN_URL, code, null); + const accessToken = tokens.accessToken(); + const userResponse = await fetch(HCID_USER_INFO_URL, { headers: { 'Authorization': `Bearer ${accessToken}`, }, }); - const slackUser: SlackUserInfo = await slackUserResponse.json(); + const userResult: HackClubUserResponse = await userResponse.json(); + const identity = userResult.identity; + + const slackId = identity.slack_id || identity.id; const existingUser = await prisma.user.findFirst({ where: { - slack_id: slackUser.sub, + slack_id: slackId, }, }); if (existingUser) { + // Update email if it's missing or changed + if (existingUser.email !== identity.primary_email) { + await prisma.user.update({ + where: { id: existingUser.id }, + data: { email: identity.primary_email }, + }); + } + const session = await lucia.createSession(existingUser.id, {}); const sessionCookie = lucia.createSessionCookie(session.id); await getRedisConnection().set(`sessions:${session.id}`, ''); @@ -52,8 +63,9 @@ export async function GET(request: Request): Promise { await prisma.user.create({ data: { id: userId, - slack_id: slackUser.sub, - pfpUrl: `https://cachet.dunkirk.sh/users/${slackUser.sub}/r`, + slack_id: slackId, + email: identity.primary_email, + pfpUrl: identity.slack_id ? `https://cachet.dunkirk.sh/users/${identity.slack_id}/r` : 'https://github.com/hackclub.png', hasOnboarded: false, }, }); @@ -83,40 +95,15 @@ export async function GET(request: Request): Promise { } } -interface SlackUserInfo { - // OpenID Connect standard fields - ok: boolean; - sub: string; - email: string; - email_verified: boolean; - date_email_verified: number; - name: string; - picture: string; - given_name: string; - family_name: string; - locale: string; - - // Slack-specific fields - ['https://slack.com/user_id']: string; - ['https://slack.com/team_id']: string; - ['https://slack.com/team_name']: string; - ['https://slack.com/team_domain']: string; - - // User image URLs - ['https://slack.com/user_image_24']: string; - ['https://slack.com/user_image_32']: string; - ['https://slack.com/user_image_48']: string; - ['https://slack.com/user_image_72']: string; - ['https://slack.com/user_image_192']: string; - ['https://slack.com/user_image_512']: string; - - // Team image URLs - ['https://slack.com/team_image_34']?: string; - ['https://slack.com/team_image_44']?: string; - ['https://slack.com/team_image_68']?: string; - ['https://slack.com/team_image_88']?: string; - ['https://slack.com/team_image_102']?: string; - ['https://slack.com/team_image_132']?: string; - ['https://slack.com/team_image_230']?: string; - ['https://slack.com/team_image_default']?: boolean; +interface HackClubIdentity { + id: string; + slack_id?: string; + first_name: string; + last_name: string; + primary_email: string; } + +interface HackClubUserResponse { + identity: HackClubIdentity; +} + diff --git a/apps/web/src/app/(ui)/(public)/auth/slack/route.ts b/apps/web/src/app/(ui)/(public)/auth/hackclub/route.ts similarity index 58% rename from apps/web/src/app/(ui)/(public)/auth/slack/route.ts rename to apps/web/src/app/(ui)/(public)/auth/hackclub/route.ts index e27d48b..d35f03a 100644 --- a/apps/web/src/app/(ui)/(public)/auth/slack/route.ts +++ b/apps/web/src/app/(ui)/(public)/auth/hackclub/route.ts @@ -1,12 +1,12 @@ import { generateState } from "arctic"; -import { slack } from '@hctv/auth'; +import { hackClub, HCID_AUTH_URL } from '@hctv/auth'; import { cookies } from "next/headers"; export async function GET(): Promise { const state = generateState(); - const url = slack.createAuthorizationURL(state, ['openid', 'profile']); + const url = hackClub.createAuthorizationURL(HCID_AUTH_URL, state, ['slack_id', 'verification_status', 'email']); - (await cookies()).set("slack_oauth_state", state, { + (await cookies()).set("hackclub_oauth_state", state, { path: "/", secure: process.env.NODE_ENV === "production", httpOnly: true, diff --git a/apps/web/src/components/app/NavBar/NavBar.tsx b/apps/web/src/components/app/NavBar/NavBar.tsx index c690255..eeb48d0 100644 --- a/apps/web/src/components/app/NavBar/NavBar.tsx +++ b/apps/web/src/components/app/NavBar/NavBar.tsx @@ -97,7 +97,7 @@ export default function Navbar(props: Props) { ) : ( - +