diff --git a/apps/web/src/app/(ui)/(public)/auth/hackclub/callback/route.ts b/apps/web/src/app/(ui)/(public)/auth/hackclub/callback/route.ts index dd3f44b..aae72ef 100644 --- a/apps/web/src/app/(ui)/(public)/auth/hackclub/callback/route.ts +++ b/apps/web/src/app/(ui)/(public)/auth/hackclub/callback/route.ts @@ -1,3 +1,4 @@ +import slackNotifier from '@/lib/services/slackNotifier'; import { hackClub, lucia, HCID_TOKEN_URL, HCID_USER_INFO_URL } from '@hctv/auth'; import { cookies as nextCookies } from 'next/headers'; import { OAuth2RequestError } from 'arctic'; @@ -49,6 +50,13 @@ export async function GET(request: Request): Promise { }); } + const slackValidation = await validateSlackUser(slackId); + if (!slackValidation.success) { + return new Response(slackValidation.message, { + status: slackValidation.status, + }); + } + const existingUser = await prisma.user.findFirst({ where: { slack_id: slackId, @@ -130,6 +138,16 @@ interface HackClubUserResponse { type VerificationStatus = 'needs_submission' | 'pending' | 'verified' | 'ineligible'; +type SlackValidationResult = + | { + success: true; + } + | { + success: false; + message: string; + status: number; + }; + function getVerificationErrorMessage(status: VerificationStatus): string { switch (status) { case 'needs_submission': @@ -142,3 +160,49 @@ function getVerificationErrorMessage(status: VerificationStatus): string { return 'Verified users can continue.'; } } + +async function validateSlackUser(slackId: string): Promise { + if (!process.env.SLACK_NOTIFIER_TOKEN) { + return { + success: false, + message: 'Slack verification is not configured right now. Please try again later.', + status: 503, + }; + } + + try { + const response = await slackNotifier.users.info({ user: slackId }); + if (!response.ok || !response.user) { + return { + success: false, + message: 'Unable to verify your Slack account right now. Please try again later.', + status: 502, + }; + } + + if (response.user.deleted) { + return { + success: false, + message: 'Your Slack account is deactivated, so you cannot access hackclub.tv.', + status: 403, + }; + } + + if (response.user.is_restricted || response.user.is_ultra_restricted) { + return { + success: false, + message: + 'Guest Slack accounts cannot access hackclub.tv. Please sign in with a full Hack Club Slack account.', + status: 403, + }; + } + + return { success: true }; + } catch { + return { + success: false, + message: 'Unable to verify your Slack account right now. Please try again later.', + status: 502, + }; + } +} diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 0e722b5..53b4214 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -10,9 +10,9 @@ export const hackClub = new OAuth2Client( process.env.HCID_REDIRECT_URI! ); -export const HCID_AUTH_URL = "https://auth.hackclub.com/oauth/authorize"; -export const HCID_TOKEN_URL = "https://auth.hackclub.com/oauth/token"; -export const HCID_USER_INFO_URL = "https://auth.hackclub.com/api/v1/me"; +export const HCID_AUTH_URL = 'https://auth.hackclub.com/oauth/authorize'; +export const HCID_TOKEN_URL = 'https://auth.hackclub.com/oauth/token'; +export const HCID_USER_INFO_URL = 'https://auth.hackclub.com/api/v1/me'; export const lucia = new Lucia(adapter, { sessionCookie: { @@ -29,6 +29,8 @@ export const lucia = new Lucia(adapter, { slack_id: attributes.slack_id, email: attributes.email, pfpUrl: attributes.pfpUrl, + hackClubVerificationResult: attributes.hackClubVerificationResult, + hackClubVerificationCheckedAt: attributes.hackClubVerificationCheckedAt, hasOnboarded: attributes.hasOnboarded, personalChannelId: attributes.personalChannelId, isAdmin: attributes.isAdmin, @@ -47,6 +49,8 @@ interface DatabaseUserAttributes { slack_id: string; email: string | null; pfpUrl: string; + hackClubVerificationResult: string | null; + hackClubVerificationCheckedAt: Date | null; hasOnboarded: boolean; personalChannelId: string | null; isAdmin: boolean; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fda63e..41ddb1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -354,6 +354,9 @@ importers: '@types/node': specifier: ^24.0.1 version: 24.10.4 + tsx: + specifier: ^4.7.1 + version: 4.21.0 typescript: specifier: ^5.8.2 version: 5.9.3 @@ -13331,8 +13334,8 @@ snapshots: '@typescript-eslint/parser': 8.51.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1) @@ -13351,7 +13354,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -13362,22 +13365,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.51.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -13388,7 +13391,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.51.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3