From 6a868d7c397fe7cd5e9f9ca13c9adc069b8b879d Mon Sep 17 00:00:00 2001 From: John Richmond <5629+jr@users.noreply.github.com> Date: Mon, 12 May 2025 23:07:31 -0700 Subject: [PATCH 1/3] Allow extensions to auth Add an endpoint to redirect to the extension with a Clerk sign in token. --- .env | 2 + .../extension/sign-in/route.ts | 25 ++++++++++++ src/lib/server/clerk.ts | 39 +++++++++++++++++++ src/lib/server/env.ts | 2 + 4 files changed, 68 insertions(+) create mode 100644 src/app/(authenticated)/extension/sign-in/route.ts create mode 100644 src/lib/server/clerk.ts diff --git a/.env b/.env index 8e3473b7e0..8e115d0fe5 100644 --- a/.env +++ b/.env @@ -1,5 +1,7 @@ BILLING_PLAN_ENV=dev +VSCODE_EXTENSION_BASE_URL=vscode://RooVeterinaryInc.roo-cline + # Clerk # https://clerk.com/docs/deployments/clerk-environment-variables#sign-in-and-sign-up-redirects NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in diff --git a/src/app/(authenticated)/extension/sign-in/route.ts b/src/app/(authenticated)/extension/sign-in/route.ts new file mode 100644 index 0000000000..bf5a2565f1 --- /dev/null +++ b/src/app/(authenticated)/extension/sign-in/route.ts @@ -0,0 +1,25 @@ +import { getSignInToken } from '@/lib/server/clerk'; +import { Env } from '@/lib/server/env'; +import { auth } from '@clerk/nextjs/server'; +import { redirect } from 'next/navigation'; +import { type NextRequest } from 'next/server'; + +export async function GET(request: NextRequest) { + const authObj = await auth(); + const userId = authObj.userId; + const state = request.nextUrl.searchParams.get('state') || ''; + + if (!userId) { + throw new Error('not logged in?'); + } + + const signInToken = await getSignInToken(authObj.userId); + if (!signInToken) { + throw new Error("couldn't sign in"); + } + + const url = new URL(`${Env.VSCODE_EXTENSION_BASE_URL}/auth/clerk/callback`); + url.searchParams.append('state', state); + url.searchParams.append('code', signInToken); + redirect(url.href); +} diff --git a/src/lib/server/clerk.ts b/src/lib/server/clerk.ts new file mode 100644 index 0000000000..fe2b3049a7 --- /dev/null +++ b/src/lib/server/clerk.ts @@ -0,0 +1,39 @@ +import { Env } from './env'; +import { logger } from './logger'; + +export async function getSignInToken( + userId: string, +): Promise { + const response = await fetch('https://api.clerk.com/v1/sign_in_tokens', { + method: 'POST', + headers: { + Authorization: `Bearer ${Env.CLERK_SECRET_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + user_id: userId, + // Default expiration is 30 days (2592000 seconds) + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + logger.error({ + event: 'sign_in_token_creation_failed', + error: errorData, + userId, + }); + + return undefined; // probably just throw + } + + const data = await response.json(); + + logger.info({ + event: 'sign_in_token_created', + userId, + data, // TODO: none of this, probably + }); + + return data.token; +} diff --git a/src/lib/server/env.ts b/src/lib/server/env.ts index 8749ac6308..6912a58da3 100644 --- a/src/lib/server/env.ts +++ b/src/lib/server/env.ts @@ -9,6 +9,7 @@ export const Env = createEnv({ STRIPE_SECRET_KEY: z.string().min(1), STRIPE_WEBHOOK_SECRET: z.string().min(1), BILLING_PLAN_ENV: z.enum(['dev', 'test', 'prod']), + VSCODE_EXTENSION_BASE_URL: z.string().min(1), }, client: { NEXT_PUBLIC_APP_URL: z.string().optional(), @@ -32,6 +33,7 @@ export const Env = createEnv({ STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, BILLING_PLAN_ENV: process.env.BILLING_PLAN_ENV, + VSCODE_EXTENSION_BASE_URL: process.env.VSCODE_EXTENSION_BASE_URL, NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, From f84169b630b9fc2d802e8a0423dd8820361950ef Mon Sep 17 00:00:00 2001 From: John Richmond <5629+jr@users.noreply.github.com> Date: Tue, 13 May 2025 14:24:18 -0700 Subject: [PATCH 2/3] Copy Matt's ping endpoint --- src/app/api/ping/route.ts | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/app/api/ping/route.ts diff --git a/src/app/api/ping/route.ts b/src/app/api/ping/route.ts new file mode 100644 index 0000000000..ac768c175e --- /dev/null +++ b/src/app/api/ping/route.ts @@ -0,0 +1,52 @@ +import { auth } from '@clerk/nextjs/server'; +import { NextResponse } from 'next/server'; + +import { logger } from '@/lib/server/logger'; + +/** + * API endpoint for testing Clerk authentication + * Verifies/parses JWT and logs authenticated user information + */ +export async function GET() { + const authObj = await auth(); + + // If not authenticated + if (!authObj.userId) { + return NextResponse.json( + { error: 'Unauthorized request' }, + { status: 401 }, + ); + } + + // Get the JWT token + const token = await authObj.getToken(); + + // Extract user information from the auth object + // Only include properties that exist on the auth object + const userInfo = { + userId: authObj.userId, + sessionId: authObj.sessionId, + orgId: authObj.orgId, + orgRole: authObj.orgRole, + // Note: To get additional user data like email, firstName, lastName, + // you would need to use Clerk's methods like clerkClient.users.getUser() + }; + + // Log the user information + logger.info({ + event: 'ping_endpoint_accessed', + userInfo: { + userId: authObj.userId, + sessionId: authObj.sessionId, + orgId: authObj.orgId, + orgRole: authObj.orgRole, + }, + hasToken: !!token, // Just log if token exists, not the actual token for security + }); + + // Return the user information + return NextResponse.json({ + authenticated: true, + userInfo, + }); +} From e062feba73050cad0d82e11cf90b8f06a1f68f98 Mon Sep 17 00:00:00 2001 From: John Richmond <5629+jr@users.noreply.github.com> Date: Tue, 13 May 2025 14:46:33 -0700 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- src/app/(authenticated)/extension/sign-in/route.ts | 2 +- src/lib/server/clerk.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/(authenticated)/extension/sign-in/route.ts b/src/app/(authenticated)/extension/sign-in/route.ts index bf5a2565f1..99f59233d9 100644 --- a/src/app/(authenticated)/extension/sign-in/route.ts +++ b/src/app/(authenticated)/extension/sign-in/route.ts @@ -10,7 +10,7 @@ export async function GET(request: NextRequest) { const state = request.nextUrl.searchParams.get('state') || ''; if (!userId) { - throw new Error('not logged in?'); + return new Response('Unauthorized', { status: 401 }); } const signInToken = await getSignInToken(authObj.userId); diff --git a/src/lib/server/clerk.ts b/src/lib/server/clerk.ts index fe2b3049a7..57ef19797f 100644 --- a/src/lib/server/clerk.ts +++ b/src/lib/server/clerk.ts @@ -24,7 +24,7 @@ export async function getSignInToken( userId, }); - return undefined; // probably just throw + throw new Error("Failed to create sign-in token"); } const data = await response.json(); @@ -32,7 +32,6 @@ export async function getSignInToken( logger.info({ event: 'sign_in_token_created', userId, - data, // TODO: none of this, probably }); return data.token;