Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 7 additions & 97 deletions claim-db-worker/README.md
Original file line number Diff line number Diff line change
@@ -1,101 +1,11 @@
# Claim DB Worker
# Claim DB

A Cloudflare Worker for claiming Prisma databases. This worker handles OAuth authentication with Prisma and transfers database projects to authenticated users.
A Next.js app for claiming and managing Prisma databases, deployed on Cloudflare Workers via OpenNext.

## Features

- ✅ OAuth authentication with Prisma
- ✅ Database project claiming
- ✅ Rate limiting (100 requests per minute)
- ✅ PostHog analytics tracking
- ✅ Cloudflare Analytics Engine integration
- ✅ Error handling and user feedback
- ✅ Responsive UI with Tailwind CSS

## Quick Start

### Development

1. **Install dependencies:**
```bash
npm install
```

2. **Set up environment variables:**
Create a `.env.local` file:
```env
CLIENT_ID=your_prisma_client_id
CLIENT_SECRET=your_prisma_client_secret
INTEGRATION_TOKEN=your_prisma_integration_token
POSTHOG_API_KEY=your_posthog_api_key
POSTHOG_API_HOST=https://app.posthog.com
```

3. **Start development server:**
```bash
npm run dev
```

4. **Test the flow:**
Visit `http://localhost:3000/?projectID=test123`

### Production Deployment

1. **Set up Cloudflare secrets:**
```bash
wrangler secret put CLIENT_ID
wrangler secret put CLIENT_SECRET
wrangler secret put INTEGRATION_TOKEN
wrangler secret put POSTHOG_API_KEY
wrangler secret put POSTHOG_API_HOST
```

2. **Build and deploy:**
```bash
npm run deploy
```

## API Endpoints

- **`/api/claim`** - Generates OAuth URLs and tracks page views
- **`/api/auth/callback`** - Handles OAuth callback and project transfer
- **`/api/test`** - Rate limit testing endpoint
- **`/api/success-test`** - Test endpoint for success page

## Pages

- **`/`** - Homepage (redirects to claim flow if projectID provided)
- **`/claim`** - Claim page with OAuth button
- **`/success`** - Success page after claiming
- **`/error`** - Error page for various error states

## Flow

1. User visits `/?projectID=123`
2. Homepage redirects to `/api/claim?projectID=123`
3. API generates OAuth URL and redirects to `/claim?projectID=123&authUrl=...`
4. User clicks OAuth, redirects to `/api/auth/callback`
5. API exchanges code for token and transfers project
6. API redirects to `/success?projectID=123`

## Configuration

The project uses Cloudflare Worker bindings:
- Rate limiting via `CLAIM_DB_RATE_LIMITER` binding
- Analytics via `CREATE_DB_DATASET` binding
- Environment variables as secrets

See `wrangler.jsonc` for the complete configuration.

## Development vs Production

- **Development**: Uses `process.env` with graceful fallbacks for rate limiting and analytics
- **Production**: Uses Cloudflare Worker bindings via `globalThis`

The `lib/env.ts` utility handles this automatically.

## Testing

- **Local testing**: `npm run dev` then visit with projectID
- **Rate limit testing**: Visit `/api/test`
- **Success page testing**: Visit `/api/success-test`
- Claim Prisma databases with one click
- Monaco-based Prisma schema editor
- Deployed globally on Cloudflare's edge network
- Prisma Studio embed
- Built with Next.js and React
34 changes: 34 additions & 0 deletions claim-db-worker/app/api/auth/url/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
const body: { redirectUri: string } = await request.json();
const redirectUri = body.redirectUri;
const clientId = process.env.CLIENT_ID;

if (!clientId) {
return NextResponse.json(
{ error: "Client ID not configured" },
{ status: 500 }
);
}

const searchParams = new URLSearchParams();
searchParams.set("client_id", clientId);
searchParams.set("redirect_uri", redirectUri.toString());
searchParams.set("response_type", "code");
searchParams.set("scope", "workspace:admin");
searchParams.set("state", generateState());
searchParams.set("utm_source", "create-db-frontend");
searchParams.set("utm_medium", "claim_button");

const authUrl = `https://auth.prisma.io/authorize?${searchParams.toString()}`;

return NextResponse.json({ authUrl });
}

function generateState(): string {
return (
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
);
}
51 changes: 24 additions & 27 deletions claim-db-worker/app/claim/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,39 @@ function ClaimContent() {
const searchParams = useSearchParams();
const router = useRouter();
const projectID = searchParams.get("projectID");
const utmSource = searchParams.get("utm_source");
const utmMedium = searchParams.get("utm_medium");
const [isLoading, setIsLoading] = useState(false);

if (!projectID && !window.location.pathname.includes('/test/')) {
if (!projectID && !window.location.pathname.includes("/test/")) {
router.push("/");
return null;
}

const redirectUri = new URL("/api/auth/callback", window.location.origin);
redirectUri.searchParams.set("projectID", projectID!);

function generateState(): string {
return (
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
);
}

const authParams = new URLSearchParams({
client_id: process.env.NEXT_PUBLIC_CLIENT_ID!,
redirect_uri: redirectUri.toString(),
response_type: "code",
scope: "workspace:admin",
state: generateState(),
utm_source: utmSource || "unknown",
utm_medium: utmMedium || "unknown",
});
const handleClaimClick = async () => {
try {
setIsLoading(true);
const response = await fetch("/api/auth/url", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ redirectUri: redirectUri.toString() }),
});

const authUrl = `https://auth.prisma.io/authorize?${authParams.toString()}`;
if (!response.ok) {
throw new Error("Failed to get auth URL");
}

const handleClaimClick = () => {
if (authUrl) {
setIsLoading(true);
window.open(authUrl, "_blank");
setTimeout(() => setIsLoading(false), 1000);
const data = (await response.json()) as { authUrl: string };
if (data.authUrl) {
window.open(data.authUrl, "_blank");
}
} catch (error) {
console.error("Error:", error);
} finally {
setIsLoading(false);
}
};

Expand All @@ -60,7 +57,7 @@ function ClaimContent() {

<button
onClick={handleClaimClick}
disabled={!authUrl || isLoading}
disabled={isLoading}
className="flex items-center justify-center gap-3 bg-[#24bfa7] hover:bg-[#16A394] disabled:opacity-50 disabled:cursor-not-allowed text-white font-bold text-xl sm:text-2xl lg:text-3xl border-none rounded-lg px-8 py-4 sm:px-10 sm:py-5 lg:px-12 lg:py-6 cursor-pointer shadow-lg transition-all duration-200 mb-16 min-h-[44px] sm:min-h-[52px] lg:min-h-[60px] mx-auto"
>
<Image
Expand All @@ -70,7 +67,7 @@ function ClaimContent() {
height={24}
className="w-6 h-6"
/>
{isLoading ? "Redirecting..." : "Claim database"}
Claim database
<Image
src="/arrow-right.svg"
alt="Arrow Right"
Expand Down
4 changes: 4 additions & 0 deletions claim-db-worker/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
"observability": {
"enabled": true,
},
"vars": {
"NEXT_PUBLIC_CLIENT_ID": "cmck0cre900ffxz0vy5deit8y",
"CLIENT_ID": "cmck0cre900ffxz0vy5deit8y",
},
"analytics_engine_datasets": [
{
"binding": "CREATE_DB_DATASET",
Expand Down