Skip to content
Open
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
49 changes: 36 additions & 13 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,20 +1,43 @@
# Neon PostgreSQL Database
DATABASE_URL=postgresql://user:password@host/dbname?sslmode=require

# GitHub OAuth App
# Create at: https://github.com/settings/developers
GITHUB_CLIENT_ID=0v23li58m3t8TIbfIr8A
# Optional fallback for older deployments. Client-side/public only, not a secret.
NEXT_PUBLIC_GITHUB_CLIENT_ID=Ov231iS8m3t8TIbfIr8A
GITHUB_CLIENT_SECRET=your_github_oauth_client_secret
# Public URL of your app (used for OAuth callback redirects)
NEXT_PUBLIC_APP_URL=https://repofuse.com

# Public URL of your app (used for OAuth callback redirect)
NEXT_PUBLIC_APP_URL=https://repo-app-architect.vercel.app
# ── GitHub OAuth ──────────────────────────────────────────────────────────────
# Create at: https://github.com/settings/developers → "New OAuth App"
# Homepage URL: https://repofuse.com
# Callback URL: https://repofuse.com/api/auth/github/callback
GITHUB_CLIENT_ID=your_github_client_id
NEXT_PUBLIC_GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret

# OpenAI API Key (used by Vercel AI SDK for analysis)
OPENAI_API_KEY=sk-...
# ── GitLab OAuth ──────────────────────────────────────────────────────────────
# Create at: https://gitlab.com/-/profile/applications → "Add new application"
# Name: RepoFuse
# Redirect URI: https://repofuse.com/api/auth/gitlab/callback
# Scopes: read_user read_repository
GITLAB_CLIENT_ID=your_gitlab_application_id
GITLAB_CLIENT_SECRET=your_gitlab_secret

# Anthropic API Key (analysis + scaffold generation)
# ── Anthropic AI (analysis + scaffold generation) ─────────────────────────────
# Get your key at: https://console.anthropic.com/
ANTHROPIC_API_KEY=sk-ant-...
# Optional override for analysis + scaffold (default: Claude Sonnet 4.5 snapshot)
# ANTHROPIC_ANALYSIS_MODEL=claude-sonnet-4-5-20250929
# Optional: override the model used for analysis (default: claude-sonnet-4-6)
# ANTHROPIC_ANALYSIS_MODEL=claude-sonnet-4-6

# ── Stripe (billing) ──────────────────────────────────────────────────────────
# Get keys at: https://dashboard.stripe.com/apikeys
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
# Create a product + recurring price at: https://dashboard.stripe.com/products
# New product → add a Recurring price of $19/month → copy the price ID (starts with price_)
STRIPE_PRO_PRICE_ID=price_...
# Register your webhook at: https://dashboard.stripe.com/webhooks
# Endpoint URL: https://repofuse.com/api/stripe/webhook
# Events to listen for: checkout.session.completed, customer.subscription.updated,
# customer.subscription.deleted, invoice.payment_failed

# ── Vercel Blob (export storage) ──────────────────────────────────────────────
BLOB_READ_WRITE_TOKEN=vercel_blob_...
31 changes: 28 additions & 3 deletions app/api/analyses/[id]/run/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NextRequest } from 'next/server'
import Anthropic from '@anthropic-ai/sdk'
import { z } from 'zod'
import { getCurrentAccessToken } from '@/lib/auth'
import { getCurrentUser } from '@/lib/auth'
import {
getGitHubRepositoryTree,
getGitHubRepositoryTreeFromBranch,
Expand All @@ -14,9 +14,12 @@ import {
createRepoFile,
createBlueprint,
deleteBlueprintsByAnalysis,
getBlueprintsByAnalysis
getBlueprintsByAnalysis,
getSubscriptionByGithubId,
incrementAnalysisUsage,
} from '@/lib/queries'
import { getAnthropicModel } from '@/lib/anthropic-model'
import { PLANS } from '@/lib/stripe'

// Schema for AI-generated app blueprints
const complexityEnum = z.preprocess((val) => {
Expand Down Expand Up @@ -140,12 +143,31 @@ export async function POST(
}

try {
const accessToken = await getCurrentAccessToken()
const user = await getCurrentUser()
const accessToken = user?.access_token ?? null
if (!accessToken) {
send({ error: 'Sign in with GitHub before running an analysis.' })
controller.close()
return
}

// Enforce plan limits
if (user) {
try {
const sub = await getSubscriptionByGithubId(user.github_id)
const planKey = sub?.plan === 'pro' ? 'pro' : 'free'
const limit = PLANS[planKey].analyses_per_month
const used = sub?.analyses_used_this_month ?? 0
if (limit !== -1 && used >= limit) {
send({ error: `You've used all ${limit} analyses for this month. Upgrade to Pro for unlimited analyses.` })
controller.close()
return
}
} catch {
// DB unavailable — allow the run
}
}

if (!process.env.ANTHROPIC_API_KEY) {
send({ error: 'AI analysis is not configured. Missing ANTHROPIC_API_KEY.' })
controller.close()
Expand Down Expand Up @@ -175,6 +197,9 @@ export async function POST(
// Update status to scanning
await updateAnalysisStatus(id, 'scanning')
await deleteBlueprintsByAnalysis(id)
if (user) {
try { await incrementAnalysisUsage(user.github_id) } catch { /* DB unavailable */ }
}
send({ status: 'scanning', progress: 10 })

// Fetch file trees from GitHub for each repository
Expand Down
84 changes: 84 additions & 0 deletions app/api/auth/bitbucket/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { NextRequest, NextResponse } from 'next/server'
import { cookies } from 'next/headers'

function getBaseUrl(request: NextRequest) {
return process.env.NEXT_PUBLIC_APP_URL || request.nextUrl.origin
}

export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const code = searchParams.get('code')
const state = searchParams.get('state')
const error = searchParams.get('error')
const cookieStore = await cookies()
const savedState = cookieStore.get('bitbucket_oauth_state')?.value
const from = cookieStore.get('bitbucket_oauth_from')?.value
const errorBase = from === 'dashboard' ? '/dashboard/repositories' : '/'

if (error) {
return NextResponse.redirect(new URL(`${errorBase}?error=bitbucket_oauth_failed`, getBaseUrl(request)))
}

if (!code) {
return NextResponse.redirect(new URL(`${errorBase}?error=missing_code`, getBaseUrl(request)))
}

if (!state || !savedState || state !== savedState) {
return NextResponse.redirect(new URL(`${errorBase}?error=invalid_oauth_state`, getBaseUrl(request)))
}

const clientId = process.env.BITBUCKET_CLIENT_ID
const clientSecret = process.env.BITBUCKET_CLIENT_SECRET

if (!clientId || !clientSecret) {
return NextResponse.redirect(new URL(`${errorBase}?error=bitbucket_oauth_not_configured`, getBaseUrl(request)))
}

const redirectUri = `${getBaseUrl(request)}/api/auth/bitbucket/callback`

// Bitbucket uses HTTP Basic Auth for token exchange
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
const tokenResponse = await fetch('https://bitbucket.org/site/oauth2/access_token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${credentials}`,
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri,
}).toString(),
})

if (!tokenResponse.ok) {
return NextResponse.redirect(new URL(`${errorBase}?error=token_exchange_failed`, getBaseUrl(request)))
}

const tokenJson = (await tokenResponse.json()) as { access_token?: string; error?: string }
const access_token = tokenJson.access_token

if (!access_token) {
return NextResponse.redirect(new URL(`${errorBase}?error=token_exchange_failed`, getBaseUrl(request)))
}

const response = NextResponse.redirect(
new URL('/dashboard/repositories?connected=bitbucket', getBaseUrl(request))
)

response.cookies.set('bitbucket_access_token', access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24 * 30,
})
response.cookies.set('bitbucket_oauth_state', '', { path: '/', maxAge: 0 })
response.cookies.set('bitbucket_oauth_from', '', { path: '/', maxAge: 0 })

return response
} catch {
return NextResponse.redirect(new URL('/?error=oauth_callback_failed', getBaseUrl(request)))
}
}
47 changes: 47 additions & 0 deletions app/api/auth/bitbucket/login/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import crypto from 'node:crypto'
import { NextRequest, NextResponse } from 'next/server'

function getBaseUrl(request: NextRequest) {
return process.env.NEXT_PUBLIC_APP_URL || request.nextUrl.origin
}

export async function GET(request: NextRequest) {
const clientId = process.env.BITBUCKET_CLIENT_ID
const from = request.nextUrl.searchParams.get('from')
const errorBase = from === 'dashboard' ? '/dashboard/repositories' : '/'

if (!clientId) {
return NextResponse.redirect(new URL(`${errorBase}?error=bitbucket_oauth_not_configured`, getBaseUrl(request)))
}

const state = crypto.randomUUID()
const redirectUri = `${getBaseUrl(request)}/api/auth/bitbucket/callback`

const params = new URLSearchParams({
client_id: clientId,
redirect_uri: redirectUri,
response_type: 'code',
scope: 'repository account',
state,
})

const response = NextResponse.redirect(
`https://bitbucket.org/site/oauth2/authorize?${params.toString()}`
)
response.cookies.set('bitbucket_oauth_state', state, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 60 * 10,
})
response.cookies.set('bitbucket_oauth_from', from ?? '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 60 * 10,
})

return response
}
116 changes: 0 additions & 116 deletions app/api/auth/connect-platform/route.ts

This file was deleted.

Loading