Events API

This commit is contained in:
cte 2025-05-13 21:50:55 -07:00
parent e062feba73
commit 70d7425ccb
15 changed files with 209 additions and 108 deletions

1
.env
View file

@ -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

View file

@ -1,4 +1,4 @@
import '../src/styles/global.css';
import '../src/styles/globals.css';
import type { Preview } from '@storybook/react';

View file

@ -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 (
<div className="flex flex-col justify-center gap-4 max-w-md h-screen mx-auto">
<Link href="/" className="text-center">
<Logo />
</Link>
<Card>
<CardContent className="flex flex-col gap-4">
<div>
You&apos;ve successfully authenticated. We&apos;re attempting to
open VSCode with your credentials.
</div>
<div className="flex flex-row justify-between gap-4">
<Button variant="outline" className="flex-1" asChild>
<Link href={vsCodeUrl}>Open in VSCode</Link>
</Button>
<Button variant="outline" className="flex-1" asChild>
<Link href={cursorUrl}>Open in Cursor</Link>
</Button>
</div>
</CardContent>
<CardFooter className="text-sm text-muted-foreground">
{redirectAttempted
? 'You can close this window after VSCode opens.'
: 'Redirecting automatically...'}
</CardFooter>
</Card>
</div>
);
};

View file

@ -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 <DeepLink vsCodeUrl={vsCodeUrl.href} cursorUrl={cursorUrl.href} />;
}

View file

@ -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);
}

View file

@ -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 });
}

View file

@ -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);
}

View file

@ -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: {

View file

@ -0,0 +1,92 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
function Card({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card"
className={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-sm border py-6 shadow-sm',
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-header"
className={cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-title"
className={cn('leading-none font-semibold', className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-description"
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-action"
className={cn(
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-content"
className={cn('px-6', className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-footer"
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

View file

@ -1,3 +1,4 @@
export * from './button';
export * from './card';
export * from './dropdown-menu';
export * from './separator';

View file

@ -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,

View file

@ -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<typeof ORG_ROLE>;
export const ORG_PERMISSION = {
// Add Organization Permissions here.
} as const;
export type OrgPermission = EnumValues<typeof ORG_PERMISSION>;

View file

@ -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<typeof PLAN_ID>;
export const BILLING_INTERVAL = {
MONTH: 'month',
YEAR: 'year',
} as const;
export type BillingInterval = EnumValues<typeof BILLING_INTERVAL>;
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;
};

View file

@ -1 +0,0 @@
export type EnumValues<Type> = Type[keyof Type];

View file

@ -1,8 +0,0 @@
import type { OrgPermission, OrgRole } from '@/types/auth';
declare global {
interface ClerkAuthorization {
permission: OrgPermission;
role: OrgRole;
}
}