Skip to content
Merged

auth #27

Show file tree
Hide file tree
Changes from 11 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
77 changes: 50 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A template for building AI-powered coding agents that supports Claude Code, Open

You can deploy your own version of the coding agent template to Vercel with one click:

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fcoding-agent-template&env=POSTGRES_URL,ANTHROPIC_API_KEY,GITHUB_TOKEN,VERCEL_TEAM_ID,VERCEL_PROJECT_ID,VERCEL_TOKEN,AI_GATEWAY_API_KEY&envDescription=Required+environment+variables+for+the+coding+agent+template.+Optional+variables+(CURSOR_API_KEY+for+Cursor+agent,+NPM_TOKEN+for+private+packages)+can+be+added+later+in+your+Vercel+project+settings.&project-name=coding-agent-template&repository-name=coding-agent-template)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fcoding-agent-template&env=POSTGRES_URL,VERCEL_TOKEN,VERCEL_TEAM_ID,VERCEL_PROJECT_ID,JWE_SECRET,ENCRYPTION_KEY&envDescription=Required+infrastructure+environment+variables.+You+will+also+need+to+configure+OAuth+(Vercel+or+GitHub)+for+user+authentication.+Optional+API+keys+can+be+added+later.&project-name=coding-agent-template&repository-name=coding-agent-template)

## Features

Expand Down Expand Up @@ -41,22 +41,54 @@ pnpm install

Create a `.env.local` file with your values:

Required environment variables:
#### Required Environment Variables (App Infrastructure)

These are set once by you (the app developer) and are used for core infrastructure:

- `POSTGRES_URL`: Your PostgreSQL connection string (works with any PostgreSQL database)
- `ANTHROPIC_API_KEY`: Your Anthropic API key for Claude
- `GITHUB_TOKEN`: GitHub personal access token (for repository access)
- `VERCEL_TEAM_ID`: Your Vercel team ID
- `VERCEL_PROJECT_ID`: Your Vercel project ID
- `VERCEL_TOKEN`: Your Vercel API token
- `AI_GATEWAY_API_KEY`: Your AI Gateway API key for AI-generated branch names and Codex agent support
- `VERCEL_TOKEN`: Your Vercel API token (for creating sandboxes)
- `VERCEL_TEAM_ID`: Your Vercel team ID (for sandbox creation)
- `VERCEL_PROJECT_ID`: Your Vercel project ID (for sandbox creation)
- `JWE_SECRET`: Base64-encoded secret for session encryption (generate with: `openssl rand -base64 32`)
- `ENCRYPTION_KEY`: 32-byte hex string for encrypting user API keys and tokens (generate with: `openssl rand -hex 32`)

#### User Authentication (Required)

**You must configure at least one authentication method** (Vercel or GitHub):

**Option 1: Sign in with Vercel**
- `VERCEL_CLIENT_ID`: Your Vercel OAuth app client ID
- `VERCEL_CLIENT_SECRET`: Your Vercel OAuth app client secret

**Option 2: Sign in with GitHub**
- `GITHUB_CLIENT_ID`: Your GitHub OAuth app client ID
- `GITHUB_CLIENT_SECRET`: Your GitHub OAuth app client secret
- `NEXT_PUBLIC_GITHUB_CLIENT_ID`: Your GitHub OAuth app client ID (same as above, exposed to client)

**You can enable both** to let users choose their preferred sign-in method.

#### API Keys (Optional - Can be per-user)

Optional environment variables:
These API keys can be set globally (fallback for all users) or left unset to require users to provide their own:

- `ANTHROPIC_API_KEY`: Anthropic API key for Claude agent (users can override in their profile)
- `AI_GATEWAY_API_KEY`: AI Gateway API key for branch name generation and Codex (users can override)
- `CURSOR_API_KEY`: For Cursor agent support (users can override)
- `GEMINI_API_KEY`: For Google Gemini agent support (users can override)
- `OPENAI_API_KEY`: For Codex and OpenCode agents (users can override)

> **Note**: Users can provide their own API keys in their profile settings, which take precedence over global environment variables.

#### GitHub Repository Access

- ~~`GITHUB_TOKEN`~~: **No longer needed!** Users authenticate with their own GitHub accounts.
- Users who sign in with GitHub automatically get repository access via their OAuth token
- Users who sign in with Vercel can connect their GitHub account from their profile

#### Optional Environment Variables

- `CURSOR_API_KEY`: For Cursor agent support
- `GEMINI_API_KEY`: For Google Gemini agent support
- `NPM_TOKEN`: For private npm packages
- `ENCRYPTION_KEY`: 32-byte hex string for encrypting MCP OAuth secrets (required only when using MCP connectors). Generate with: `openssl rand -hex 32`
- `GITHUB_TOKEN`: Only needed if you want to provide fallback repository access for Vercel users who haven't connected GitHub

### 4. Set up the database

Expand Down Expand Up @@ -93,22 +125,13 @@ Open [http://localhost:3000](http://localhost:3000) in your browser.

## Environment Variables

### Required

- `POSTGRES_URL`: PostgreSQL connection string
- `ANTHROPIC_API_KEY`: Claude API key
- `GITHUB_TOKEN`: GitHub token for repository access
- `VERCEL_TEAM_ID`: Vercel team ID for sandbox creation
- `VERCEL_PROJECT_ID`: Vercel project ID for sandbox creation
- `VERCEL_TOKEN`: Vercel API token for sandbox creation
- `AI_GATEWAY_API_KEY`: AI Gateway API key for branch name generation and Codex agent support

### Optional
See the [Set up environment variables](#3-set-up-environment-variables) section above for a complete guide.

- `CURSOR_API_KEY`: Cursor agent API key
- `GEMINI_API_KEY`: Google Gemini agent API key (get yours at [Google AI Studio](https://aistudio.google.com/apikey))
- `NPM_TOKEN`: NPM token for private packages
- `ENCRYPTION_KEY`: 32-byte hex string for encrypting MCP OAuth secrets (required only when using MCP connectors). Generate with: `openssl rand -hex 32`
**Key Points:**
- **Infrastructure**: Set `POSTGRES_URL`, `VERCEL_TOKEN`, `VERCEL_TEAM_ID`, `VERCEL_PROJECT_ID`, `JWE_SECRET`, and `ENCRYPTION_KEY` as the app developer
- **Authentication**: Configure at least one OAuth method (Vercel or GitHub) for user sign-in
- **API Keys**: Can be set globally or left for users to provide their own (per-user keys take precedence)
- **GitHub Access**: Users authenticate with their own GitHub accounts - no shared `GITHUB_TOKEN` needed!

## AI Branch Name Generation

Expand Down
135 changes: 135 additions & 0 deletions app/api/api-keys/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { NextRequest, NextResponse } from 'next/server'
import { getSessionFromReq } from '@/lib/session/server'
import { db } from '@/lib/db/client'
import { userConnections } from '@/lib/db/schema'
import { eq, and } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { encrypt, decrypt } from '@/lib/crypto'

Check warning on line 7 in app/api/api-keys/route.ts

View workflow job for this annotation

GitHub Actions / checks

'decrypt' is defined but never used

type Provider = 'openai' | 'gemini' | 'cursor' | 'anthropic' | 'aigateway'

export async function GET(req: NextRequest) {
try {
const session = await getSessionFromReq(req)

if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const connections = await db

Check warning on line 19 in app/api/api-keys/route.ts

View workflow job for this annotation

GitHub Actions / checks

'connections' is assigned a value but never used
.select({
provider: userConnections.provider,
createdAt: userConnections.createdAt,
})
.from(userConnections)
.where(
and(
eq(userConnections.userId, session.user.id),
// Only get API key providers (not OAuth like GitHub)
// Using SQL OR with multiple conditions
eq(userConnections.provider, 'openai'),
),
)

// Get all API key providers
const allConnections = await db
.select({
provider: userConnections.provider,
createdAt: userConnections.createdAt,
})
.from(userConnections)
.where(eq(userConnections.userId, session.user.id))

const apiKeyProviders = allConnections.filter((c) =>
['openai', 'gemini', 'cursor', 'anthropic', 'aigateway'].includes(c.provider),
)

return NextResponse.json({
success: true,
apiKeys: apiKeyProviders,
})
} catch (error) {
console.error('Error fetching API keys:', error)
return NextResponse.json({ error: 'Failed to fetch API keys' }, { status: 500 })
}
}

export async function POST(req: NextRequest) {
try {
const session = await getSessionFromReq(req)

if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const body = await req.json()
const { provider, apiKey } = body as { provider: Provider; apiKey: string }

if (!provider || !apiKey) {
return NextResponse.json({ error: 'Provider and API key are required' }, { status: 400 })
}

if (!['openai', 'gemini', 'cursor', 'anthropic'].includes(provider)) {
return NextResponse.json({ error: 'Invalid provider' }, { status: 400 })
}

// Check if connection already exists
const existing = await db
.select()
.from(userConnections)
.where(and(eq(userConnections.userId, session.user.id), eq(userConnections.provider, provider)))
.limit(1)

const encryptedKey = encrypt(apiKey)

if (existing.length > 0) {
// Update existing
await db
.update(userConnections)
.set({
accessToken: encryptedKey,
updatedAt: new Date(),
})
.where(and(eq(userConnections.userId, session.user.id), eq(userConnections.provider, provider)))
} else {
// Insert new
await db.insert(userConnections).values({
id: nanoid(),
userId: session.user.id,
provider,
accessToken: encryptedKey,
})
}

return NextResponse.json({ success: true })
} catch (error) {
console.error('Error saving API key:', error)
return NextResponse.json({ error: 'Failed to save API key' }, { status: 500 })
}
}

export async function DELETE(req: NextRequest) {
try {
const session = await getSessionFromReq(req)

if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const { searchParams } = new URL(req.url)
const provider = searchParams.get('provider') as Provider

if (!provider) {
return NextResponse.json({ error: 'Provider is required' }, { status: 400 })
}

await db
.delete(userConnections)
.where(and(eq(userConnections.userId, session.user.id), eq(userConnections.provider, provider)))

return NextResponse.json({ success: true })
} catch (error) {
console.error('Error deleting API key:', error)
return NextResponse.json({ error: 'Failed to delete API key' }, { status: 500 })
}
}
62 changes: 62 additions & 0 deletions app/api/auth/callback/vercel/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { type NextRequest } from 'next/server'
import { OAuth2Client, type OAuth2Tokens } from 'arctic'
import { createSession, saveSession } from '@/lib/session/create'
import { cookies } from 'next/headers'

export async function GET(req: NextRequest): Promise<Response> {
const code = req.nextUrl.searchParams.get('code')
const state = req.nextUrl.searchParams.get('state')
const cookieStore = await cookies()
const storedState = cookieStore.get(`vercel_oauth_state`)?.value ?? null
const storedVerifier = cookieStore.get(`vercel_oauth_code_verifier`)?.value ?? null
const storedRedirectTo = cookieStore.get(`vercel_oauth_redirect_to`)?.value ?? null

if (
code === null ||
state === null ||
storedState !== state ||
storedRedirectTo === null ||
storedVerifier === null
) {
return new Response(null, {
status: 400,
})
}

const client = new OAuth2Client(
process.env.VERCEL_CLIENT_ID ?? '',
process.env.VERCEL_CLIENT_SECRET ?? '',
`${req.nextUrl.origin}/api/auth/callback/vercel`,
)

let tokens: OAuth2Tokens

try {
tokens = await client.validateAuthorizationCode('https://vercel.com/api/login/oauth/token', code, storedVerifier)
} catch (error) {
console.error('Failed to validate authorization code:', error)
return new Response(null, {
status: 400,
})
}

const response = new Response(null, {
status: 302,
headers: {
Location: storedRedirectTo,
},
})

const session = await createSession({
accessToken: tokens.accessToken(),
expiresAt: tokens.accessTokenExpiresAt().getTime(),
})

await saveSession(response, session)

cookieStore.delete(`vercel_oauth_state`)
cookieStore.delete(`vercel_oauth_code_verifier`)
cookieStore.delete(`vercel_oauth_redirect_to`)

return response
}
Loading
Loading