mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Merge pull request #3 from RooCodeInc/cte/events-api
This commit is contained in:
commit
23087b29f5
29 changed files with 379 additions and 290 deletions
|
|
@ -6,6 +6,6 @@ set -u
|
|||
if [ -n "$POSTGRES_DATABASES" ]; then
|
||||
for db in $(echo $POSTGRES_DATABASES | tr ',' ' '); do
|
||||
echo "Creating $db..."
|
||||
psql -v ON_ERROR_STOP=1 -c "CREATE DATABASE $db;"
|
||||
psql -U postgres -v ON_ERROR_STOP=1 -c "CREATE DATABASE $db;"
|
||||
done
|
||||
fi
|
||||
|
|
|
|||
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';
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export default [
|
|||
},
|
||||
},
|
||||
{
|
||||
ignores: ['dist/**', '.next'],
|
||||
ignores: ['dist/**', '.next', 'storybook-static'],
|
||||
},
|
||||
{
|
||||
...pluginReact.configs.flat.recommended,
|
||||
|
|
|
|||
8
migrations/0000_hesitant_kylun.sql
Normal file
8
migrations/0000_hesitant_kylun.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE "events" (
|
||||
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "events_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
||||
"type" text NOT NULL,
|
||||
"timestamp" bigint NOT NULL,
|
||||
"properties" json NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
CREATE TABLE "organization" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"stripe_customer_id" text,
|
||||
"stripe_subscription_id" text,
|
||||
"stripe_subscription_price_id" text,
|
||||
"stripe_subscription_status" text,
|
||||
"stripe_subscription_current_period_end" bigint,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "stripe_customer_id_idx" ON "organization" USING btree ("stripe_customer_id");
|
||||
|
|
@ -1,55 +1,47 @@
|
|||
{
|
||||
"id": "013accf9-070a-47cd-9b49-1318a1c8a237",
|
||||
"id": "5ab333f5-a980-4564-a919-3d15608505d0",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.organization": {
|
||||
"name": "organization",
|
||||
"public.events": {
|
||||
"name": "events",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "events_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"stripe_customer_id": {
|
||||
"name": "stripe_customer_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"stripe_subscription_id": {
|
||||
"name": "stripe_subscription_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"stripe_subscription_price_id": {
|
||||
"name": "stripe_subscription_price_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"stripe_subscription_status": {
|
||||
"name": "stripe_subscription_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"stripe_subscription_current_period_end": {
|
||||
"name": "stripe_subscription_current_period_end",
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"properties": {
|
||||
"name": "properties",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
|
|
@ -57,25 +49,16 @@
|
|||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"stripe_customer_id_idx": {
|
||||
"name": "stripe_customer_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "stripe_customer_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1747031111132,
|
||||
"tag": "0000_misty_leo",
|
||||
"when": 1747204609794,
|
||||
"tag": "0000_hesitant_kylun",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
28
src/app/api/events/route.ts
Normal file
28
src/app/api/events/route.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { eventSchema } from '@/schemas';
|
||||
import { db } from '@/db';
|
||||
import { eventsTable } from '@/db/schema';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized request' },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const payload = await request.json();
|
||||
const result = eventSchema.safeParse(payload);
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json({ success: false });
|
||||
}
|
||||
|
||||
const [record] = await db.insert(eventsTable).values(result.data).returning();
|
||||
|
||||
return NextResponse.json({ success: true, id: record?.id });
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
51
src/app/extension/sign-in/DeepLink.tsx
Normal file
51
src/app/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/extension/sign-in/page.tsx
Normal file
31
src/app/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 { state } = await params.searchParams;
|
||||
|
||||
if (!state) {
|
||||
redirect(`/sign-in`);
|
||||
}
|
||||
|
||||
const { userId } = await auth();
|
||||
const code = userId ? await getSignInToken(userId) : undefined;
|
||||
|
||||
if (!code) {
|
||||
redirect(`/sign-in?state=${state}`);
|
||||
}
|
||||
|
||||
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,5 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { dark } from '@clerk/themes';
|
||||
|
|
@ -10,13 +12,22 @@ import { Logo } from '@/components/layout';
|
|||
export const SignIn = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const forceRedirectUrl = useMemo(() => {
|
||||
const state = searchParams.get('state');
|
||||
return state ? `/extension/sign-in?state=${state}` : undefined;
|
||||
}, [searchParams]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 h-screen w-full items-center justify-center">
|
||||
<Link href="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
<ClerkSignIn appearance={{ baseTheme }} />
|
||||
<ClerkSignIn
|
||||
appearance={{ baseTheme }}
|
||||
forceRedirectUrl={forceRedirectUrl}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { dark } from '@clerk/themes';
|
||||
|
|
@ -10,13 +12,22 @@ import { Logo } from '@/components/layout';
|
|||
export const SignUp = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const forceRedirectUrl = useMemo(() => {
|
||||
const state = searchParams.get('state');
|
||||
return state ? `/extension/sign-in?state=${state}` : undefined;
|
||||
}, [searchParams]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 h-screen w-full items-center justify-center">
|
||||
<Link href="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
<ClerkSignUp appearance={{ baseTheme }} />
|
||||
<ClerkSignUp
|
||||
appearance={{ baseTheme }}
|
||||
forceRedirectUrl={forceRedirectUrl}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -3,30 +3,39 @@ import {
|
|||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
json,
|
||||
integer,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
|
||||
export const organizationSchema = pgTable(
|
||||
'organization',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
stripeCustomerId: text('stripe_customer_id'),
|
||||
stripeSubscriptionId: text('stripe_subscription_id'),
|
||||
stripeSubscriptionPriceId: text('stripe_subscription_price_id'),
|
||||
stripeSubscriptionStatus: text('stripe_subscription_status'),
|
||||
stripeSubscriptionCurrentPeriodEnd: bigint(
|
||||
'stripe_subscription_current_period_end',
|
||||
{ mode: 'number' },
|
||||
),
|
||||
updatedAt: timestamp('updated_at', { mode: 'date' })
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
stripeCustomerIdIdx: uniqueIndex('stripe_customer_id_idx').on(
|
||||
table.stripeCustomerId,
|
||||
),
|
||||
}),
|
||||
);
|
||||
// export const organizationTable = pgTable(
|
||||
// 'organizations',
|
||||
// {
|
||||
// id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||
// stripeCustomerId: text('stripe_customer_id'),
|
||||
// stripeSubscriptionId: text('stripe_subscription_id'),
|
||||
// stripeSubscriptionPriceId: text('stripe_subscription_price_id'),
|
||||
// stripeSubscriptionStatus: text('stripe_subscription_status'),
|
||||
// stripeSubscriptionCurrentPeriodEnd: bigint(
|
||||
// 'stripe_subscription_current_period_end',
|
||||
// { mode: 'number' },
|
||||
// ),
|
||||
// updatedAt: timestamp('updated_at', { mode: 'date' })
|
||||
// .defaultNow()
|
||||
// .$onUpdate(() => new Date())
|
||||
// .notNull(),
|
||||
// createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(),
|
||||
// },
|
||||
// (table) => [uniqueIndex('stripe_customer_id_idx').on(table.stripeCustomerId)],
|
||||
// );
|
||||
|
||||
export const eventsTable = pgTable('events', {
|
||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||
type: text('type').notNull(),
|
||||
timestamp: bigint('timestamp', { mode: 'number' }).notNull(),
|
||||
properties: json('properties').notNull(),
|
||||
createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'date' })
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
53
src/schemas/index.ts
Normal file
53
src/schemas/index.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const providerNames = [
|
||||
'anthropic',
|
||||
'glama',
|
||||
'openrouter',
|
||||
'bedrock',
|
||||
'vertex',
|
||||
'openai',
|
||||
'ollama',
|
||||
'vscode-lm',
|
||||
'lmstudio',
|
||||
'gemini',
|
||||
'openai-native',
|
||||
'mistral',
|
||||
'deepseek',
|
||||
'unbound',
|
||||
'requesty',
|
||||
'human-relay',
|
||||
'fake-ai',
|
||||
'xai',
|
||||
'groq',
|
||||
'chutes',
|
||||
'litellm',
|
||||
] as const;
|
||||
|
||||
export const eventSchema = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('task_created'),
|
||||
timestamp: z.number(),
|
||||
properties: z.object({
|
||||
taskId: z.string(),
|
||||
provider: z.enum(providerNames),
|
||||
modelId: z.string(),
|
||||
prompt: z.string(),
|
||||
mode: z.string(),
|
||||
}),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('completion'),
|
||||
timestamp: z.number(),
|
||||
properties: z.object({
|
||||
taskId: z.string(),
|
||||
provider: z.enum(providerNames),
|
||||
modelId: z.string(),
|
||||
inputTokens: z.number(),
|
||||
outputTokens: z.number(),
|
||||
cost: z.number(),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type Event = z.infer<typeof eventSchema>;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('I18n', () => {
|
||||
test.describe('Language Switching', () => {
|
||||
test('should switch language from English to French using dropdown and verify text on the homepage', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/');
|
||||
|
||||
await expect(
|
||||
page.getByText('The perfect SaaS template to build'),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'lang-switcher' }).click();
|
||||
await page.getByText('Français').click();
|
||||
|
||||
await expect(
|
||||
page.getByText('Le parfait SaaS template pour construire'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should switch language from English to French using URL and verify text on the sign-in page', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/sign-in');
|
||||
|
||||
await expect(page.getByText('Email address')).toBeVisible();
|
||||
|
||||
await page.goto('/fr/sign-in');
|
||||
|
||||
await expect(page.getByText('Adresse e-mail')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
|
||||
// Checkly is a tool used to monitor deployed environments, such as production or preview environments.
|
||||
// It runs end-to-end tests with the `.check.e2e.ts` extension after each deployment to ensure that the environment is up and running.
|
||||
// With Checkly, you can monitor your production environment and run `*.check.e2e.ts` tests regularly at a frequency of your choice.
|
||||
// If the tests fail, Checkly will notify you via email, Slack, or other channels of your choice.
|
||||
// On the other hand, E2E tests ending with `*.e2e.ts` are only run before deployment.
|
||||
// You can run them locally or on CI to ensure that the application is ready for deployment.
|
||||
|
||||
// BaseURL needs to be explicitly defined in the test file.
|
||||
// Otherwise, Checkly runtime will throw an exception: `CHECKLY_INVALID_URL: Only URL's that start with http(s)`
|
||||
// You can't use `goto` function directly with a relative path like with other *.e2e.ts tests.
|
||||
// Check the example at https://feedback.checklyhq.com/changelog/new-changelog-436
|
||||
|
||||
test.describe('Sanity', () => {
|
||||
test.describe('Static pages', () => {
|
||||
test('should display the homepage', async ({ page, baseURL }) => {
|
||||
await page.goto(`${baseURL}/`);
|
||||
|
||||
await expect(
|
||||
page.getByText('The perfect SaaS template to build'),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import percySnapshot from '@percy/playwright';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Visual testing', () => {
|
||||
test.describe('Static pages', () => {
|
||||
test('should take screenshot of the homepage', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await expect(
|
||||
page.getByText('The perfect SaaS template to build'),
|
||||
).toBeVisible();
|
||||
|
||||
await percySnapshot(page, 'Homepage');
|
||||
});
|
||||
|
||||
test('should take screenshot of the French homepage', async ({ page }) => {
|
||||
await page.goto('/fr');
|
||||
|
||||
await expect(
|
||||
page.getByText('Le parfait SaaS template pour construire'),
|
||||
).toBeVisible();
|
||||
|
||||
await percySnapshot(page, 'Homepage - French');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1 +0,0 @@
|
|||
Place your integration tests in this folder by creating files with the `*.spec.ts` extension. These files will be automatically detected by Playwright. For a real-world example, you can check out the Next.js Boilerplate Pro at https://nextjs-boilerplate.com/pro-saas-starter-kit
|
||||
Loading…
Add table
Reference in a new issue