From 70d7425ccb65dce5f2fbd99107b38c7124f51717 Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 13 May 2025 21:50:55 -0700 Subject: [PATCH] Events API --- .env | 1 + .storybook/preview.ts | 2 +- .../extension/sign-in/DeepLink.tsx | 51 ++++++++++ .../extension/sign-in/page.tsx | 31 +++++++ .../extension/sign-in/route.ts | 25 ----- src/app/api/events/route.ts | 17 ++++ src/app/api/ping/route.ts | 42 +++------ src/components/ui/button.tsx | 2 +- src/components/ui/card.tsx | 92 +++++++++++++++++++ src/components/ui/index.ts | 1 + src/lib/server/env.ts | 2 + src/types/Auth.ts | 14 --- src/types/Subscription.ts | 28 ------ src/types/enum.ts | 1 - src/types/global.d.ts | 8 -- 15 files changed, 209 insertions(+), 108 deletions(-) create mode 100644 src/app/(authenticated)/extension/sign-in/DeepLink.tsx create mode 100644 src/app/(authenticated)/extension/sign-in/page.tsx delete mode 100644 src/app/(authenticated)/extension/sign-in/route.ts create mode 100644 src/app/api/events/route.ts create mode 100644 src/components/ui/card.tsx delete mode 100644 src/types/Auth.ts delete mode 100644 src/types/Subscription.ts delete mode 100644 src/types/enum.ts delete mode 100644 src/types/global.d.ts diff --git a/.env b/.env index 8e115d0fe5..bedfcf9eea 100644 --- a/.env +++ b/.env @@ -1,6 +1,7 @@ BILLING_PLAN_ENV=dev VSCODE_EXTENSION_BASE_URL=vscode://RooVeterinaryInc.roo-cline +CURSOR_EXTENSION_BASE_URL=cursor://RooVeterinaryInc.roo-cline # Clerk # https://clerk.com/docs/deployments/clerk-environment-variables#sign-in-and-sign-up-redirects diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 0e436a643c..211803d989 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,4 +1,4 @@ -import '../src/styles/global.css'; +import '../src/styles/globals.css'; import type { Preview } from '@storybook/react'; diff --git a/src/app/(authenticated)/extension/sign-in/DeepLink.tsx b/src/app/(authenticated)/extension/sign-in/DeepLink.tsx new file mode 100644 index 0000000000..221e20544d --- /dev/null +++ b/src/app/(authenticated)/extension/sign-in/DeepLink.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Link from 'next/link'; + +import { Button, Card, CardContent, CardFooter } from '@/components/ui'; +import { Logo } from '@/components/layout'; + +interface DeepLinkProps { + vsCodeUrl: string; + cursorUrl: string; +} + +export const DeepLink = ({ vsCodeUrl, cursorUrl }: DeepLinkProps) => { + const [redirectAttempted, setRedirectAttempted] = useState(false); + + useEffect(() => { + window.location.href = vsCodeUrl; + const timer = setTimeout(() => setRedirectAttempted(true), 2000); + return () => clearTimeout(timer); + }, [vsCodeUrl]); + + return ( +
+ + + + + +
+ You've successfully authenticated. We're attempting to + open VSCode with your credentials. +
+
+ + +
+
+ + {redirectAttempted + ? 'You can close this window after VSCode opens.' + : 'Redirecting automatically...'} + +
+
+ ); +}; diff --git a/src/app/(authenticated)/extension/sign-in/page.tsx b/src/app/(authenticated)/extension/sign-in/page.tsx new file mode 100644 index 0000000000..1aab66ee91 --- /dev/null +++ b/src/app/(authenticated)/extension/sign-in/page.tsx @@ -0,0 +1,31 @@ +import { redirect } from 'next/navigation'; +import { auth } from '@clerk/nextjs/server'; + +import { getSignInToken } from '@/lib/server/clerk'; +import { Env } from '@/lib/server/env'; + +import { DeepLink } from './DeepLink'; + +export default async function Page(params: { + searchParams: { state?: string }; +}) { + const { userId } = await auth(); + + if (!userId) { + redirect('/sign-in'); + } + + const code = await getSignInToken(userId); + + if (!code) { + redirect('/sign-in'); + } + + const { state = '' } = await params.searchParams; + const searchParams = new URLSearchParams({ state, code }); + const path = `/auth/clerk/callback?${searchParams.toString()}`; + const vsCodeUrl = new URL(path, Env.VSCODE_EXTENSION_BASE_URL); + const cursorUrl = new URL(path, Env.CURSOR_EXTENSION_BASE_URL); + + return ; +} diff --git a/src/app/(authenticated)/extension/sign-in/route.ts b/src/app/(authenticated)/extension/sign-in/route.ts deleted file mode 100644 index 99f59233d9..0000000000 --- a/src/app/(authenticated)/extension/sign-in/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -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) { - return new Response('Unauthorized', { status: 401 }); - } - - 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/app/api/events/route.ts b/src/app/api/events/route.ts new file mode 100644 index 0000000000..30fcee7f07 --- /dev/null +++ b/src/app/api/events/route.ts @@ -0,0 +1,17 @@ +import { auth } from '@clerk/nextjs/server'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + const { userId } = await auth(); + + if (!userId) { + return NextResponse.json( + { error: 'Unauthorized request' }, + { status: 401 }, + ); + } + + console.log(await request.json()); + + return NextResponse.json({ success: true }); +} diff --git a/src/app/api/ping/route.ts b/src/app/api/ping/route.ts index ac768c175e..e27943b52e 100644 --- a/src/app/api/ping/route.ts +++ b/src/app/api/ping/route.ts @@ -4,13 +4,12 @@ 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 + * 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' }, @@ -18,35 +17,18 @@ export async function GET() { ); } - // Get the JWT token + // 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() - }; + // Extract user information from the auth object. + // Only include properties that exist on the auth object. + // Note: To get additional user data like email, firstName, lastName, + // you would need to use Clerk's methods like clerkClient.users.getUser(). + const { userId, sessionId, orgId, orgRole } = authObj; + const userInfo = { userId, sessionId, orgId, orgRole }; - // 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 - }); + // Just log if token exists, not the actual token for security. + logger.info({ event: 'ping_endpoint_accessed', userInfo, hasToken: !!token }); - // Return the user information - return NextResponse.json({ - authenticated: true, - userInfo, - }); + return NextResponse.json(userInfo); } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 29f98ef9ee..903736b26f 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer", { variants: { variant: { diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000000..7cda3847f4 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Card({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardAction({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +}; diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index b64503aeb9..e395365f1f 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -1,3 +1,4 @@ export * from './button'; +export * from './card'; export * from './dropdown-menu'; export * from './separator'; diff --git a/src/lib/server/env.ts b/src/lib/server/env.ts index 6912a58da3..d27e9e92ee 100644 --- a/src/lib/server/env.ts +++ b/src/lib/server/env.ts @@ -10,6 +10,7 @@ export const Env = createEnv({ STRIPE_WEBHOOK_SECRET: z.string().min(1), BILLING_PLAN_ENV: z.enum(['dev', 'test', 'prod']), VSCODE_EXTENSION_BASE_URL: z.string().min(1), + CURSOR_EXTENSION_BASE_URL: z.string().min(1), }, client: { NEXT_PUBLIC_APP_URL: z.string().optional(), @@ -34,6 +35,7 @@ export const Env = createEnv({ 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, + CURSOR_EXTENSION_BASE_URL: process.env.CURSOR_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, diff --git a/src/types/Auth.ts b/src/types/Auth.ts deleted file mode 100644 index 7903e44bfd..0000000000 --- a/src/types/Auth.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { EnumValues } from './enum'; - -export const ORG_ROLE = { - ADMIN: 'org:admin', - MEMBER: 'org:member', -} as const; - -export type OrgRole = EnumValues; - -export const ORG_PERMISSION = { - // Add Organization Permissions here. -} as const; - -export type OrgPermission = EnumValues; diff --git a/src/types/Subscription.ts b/src/types/Subscription.ts deleted file mode 100644 index c35827213a..0000000000 --- a/src/types/Subscription.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { EnumValues } from './enum'; - -export const PLAN_ID = { - FREE: 'free', - PREMIUM: 'premium', - ENTERPRISE: 'enterprise', -} as const; - -export type PlanId = EnumValues; - -export const BILLING_INTERVAL = { - MONTH: 'month', - YEAR: 'year', -} as const; - -export type BillingInterval = EnumValues; - -export const SUBSCRIPTION_STATUS = { - ACTIVE: 'active', - PENDING: 'pending', -} as const; - -export type IStripeSubscription = { - stripeSubscriptionId: string | null; - stripeSubscriptionPriceId: string | null; - stripeSubscriptionStatus: string | null; - stripeSubscriptionCurrentPeriodEnd: number | null; -}; diff --git a/src/types/enum.ts b/src/types/enum.ts deleted file mode 100644 index c273087f69..0000000000 --- a/src/types/enum.ts +++ /dev/null @@ -1 +0,0 @@ -export type EnumValues = Type[keyof Type]; diff --git a/src/types/global.d.ts b/src/types/global.d.ts deleted file mode 100644 index 5c887a534f..0000000000 --- a/src/types/global.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { OrgPermission, OrgRole } from '@/types/auth'; - -declare global { - interface ClerkAuthorization { - permission: OrgPermission; - role: OrgRole; - } -}