diff --git a/apps/web/src/trpc/commands/github/mutations.test.ts b/apps/web/src/trpc/commands/github/mutations.test.ts index dfdfbac9a..a72e73b0e 100644 --- a/apps/web/src/trpc/commands/github/mutations.test.ts +++ b/apps/web/src/trpc/commands/github/mutations.test.ts @@ -69,6 +69,7 @@ vi.stubGlobal('fetch', mockFetch); import { finishCreateGitHubAppManifestCommand, + startAuthenticateGitHubAccountCommand, startCreateGitHubInstallationCommand, startCreateGitHubAppManifestCommand, } from './mutations'; @@ -434,3 +435,52 @@ describe('GitHub App manifest commands', () => { expect(mockUpsertDeploymentEnvironmentVariables).not.toHaveBeenCalled(); }); }); + +describe('startAuthenticateGitHubAccountCommand', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('builds the authorize URL from the deployment-resolved client id', async () => { + mockResolveDeploymentEnvVar.mockImplementation(async (name: string) => + name === 'GITHUB_CLIENT_ID' ? 'Iv1.resolved-client' : null, + ); + + const result = await startAuthenticateGitHubAccountCommand( + buildMockAuth(), + { redirect: '/settings?tab=account' }, + ); + + expect(result.success).toBe(true); + + if (!result.success) { + return; + } + + const url = new URL(result.url); + expect(`${url.origin}${url.pathname}`).toBe( + 'https://github.com/login/oauth/authorize', + ); + expect(url.searchParams.get('client_id')).toBe('Iv1.resolved-client'); + expect(url.searchParams.get('scope')).toBe('read:user'); + expect(url.searchParams.get('redirect_uri')).toBe( + 'https://roomote.example.com/github/callback', + ); + expect(decodeRecord(url.searchParams.get('state') ?? '')).toEqual({ + redirect: '/settings?tab=account', + mode: 'auth', + }); + }); + + it('refuses to link an account when no GitHub App client id is configured', async () => { + mockResolveDeploymentEnvVar.mockResolvedValue(null); + + const result = await startAuthenticateGitHubAccountCommand(buildMockAuth()); + + expect(result).toEqual({ + success: false, + error: + 'Configure a GitHub App for this deployment before linking your GitHub account. Create one or enter its credentials first.', + }); + }); +}); diff --git a/apps/web/src/trpc/commands/github/mutations.ts b/apps/web/src/trpc/commands/github/mutations.ts index 72690043e..f34e6d484 100644 --- a/apps/web/src/trpc/commands/github/mutations.ts +++ b/apps/web/src/trpc/commands/github/mutations.ts @@ -343,9 +343,26 @@ async function getGitHubOAuthUser({ } | { success: false; error: string } > { + const [clientId, clientSecret] = await Promise.all([ + resolveDeploymentEnvVar('GITHUB_CLIENT_ID'), + resolveDeploymentEnvVar('GITHUB_CLIENT_SECRET'), + ]); + + if (!clientId || !clientSecret) { + console.error( + `[${context}] GitHub App OAuth credentials are not configured.`, + ); + + return { + success: false, + error: + 'GitHub App OAuth credentials are not configured for this deployment.', + }; + } + const params = new URLSearchParams({ - client_id: Env.GITHUB_CLIENT_ID, - client_secret: Env.GITHUB_CLIENT_SECRET, + client_id: clientId, + client_secret: clientSecret, code, }); @@ -778,9 +795,19 @@ export async function startAuthenticateGitHubAccountCommand( state?: Record, ): Promise<{ success: true; url: string } | { success: false; error: string }> { try { + const clientId = await resolveDeploymentEnvVar('GITHUB_CLIENT_ID'); + + if (!clientId) { + return { + success: false, + error: + 'Configure a GitHub App for this deployment before linking your GitHub account. Create one or enter its credentials first.', + }; + } + const baseUrl = Env.ROOMOTE_APP_URL; const params = new URLSearchParams({ - client_id: Env.GITHUB_CLIENT_ID, + client_id: clientId, scope: 'read:user', state: encodeRecord({ ...(state ?? {}), mode: 'auth' }), });