mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Events API
This commit is contained in:
parent
e062feba73
commit
70d7425ccb
15 changed files with 209 additions and 108 deletions
1
.env
1
.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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import '../src/styles/global.css';
|
||||
import '../src/styles/globals.css';
|
||||
|
||||
import type { Preview } from '@storybook/react';
|
||||
|
||||
|
|
|
|||
51
src/app/(authenticated)/extension/sign-in/DeepLink.tsx
Normal file
51
src/app/(authenticated)/extension/sign-in/DeepLink.tsx
Normal 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've successfully authenticated. We'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>
|
||||
);
|
||||
};
|
||||
31
src/app/(authenticated)/extension/sign-in/page.tsx
Normal file
31
src/app/(authenticated)/extension/sign-in/page.tsx
Normal 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} />;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
17
src/app/api/events/route.ts
Normal file
17
src/app/api/events/route.ts
Normal 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 });
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal 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,
|
||||
};
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './button';
|
||||
export * from './card';
|
||||
export * from './dropdown-menu';
|
||||
export * from './separator';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
export type EnumValues<Type> = Type[keyof Type];
|
||||
8
src/types/global.d.ts
vendored
8
src/types/global.d.ts
vendored
|
|
@ -1,8 +0,0 @@
|
|||
import type { OrgPermission, OrgRole } from '@/types/auth';
|
||||
|
||||
declare global {
|
||||
interface ClerkAuthorization {
|
||||
permission: OrgPermission;
|
||||
role: OrgRole;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue