Skip to content
Merged

auth #27

Show file tree
Hide file tree
Changes from 18 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
346 changes: 317 additions & 29 deletions README.md

Large diffs are not rendered by default.

113 changes: 113 additions & 0 deletions app/api/api-keys/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { NextRequest, NextResponse } from 'next/server'
import { getSessionFromReq } from '@/lib/session/server'
import { db } from '@/lib/db/client'
import { keys } 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 userKeys = await db
.select({
provider: keys.provider,
createdAt: keys.createdAt,
})
.from(keys)
.where(eq(keys.userId, session.user.id))

return NextResponse.json({
success: true,
apiKeys: userKeys,
})
} 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', 'aigateway'].includes(provider)) {
return NextResponse.json({ error: 'Invalid provider' }, { status: 400 })
}

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

const encryptedKey = encrypt(apiKey)

if (existing.length > 0) {
// Update existing
await db
.update(keys)
.set({
value: encryptedKey,
updatedAt: new Date(),
})
.where(and(eq(keys.userId, session.user.id), eq(keys.provider, provider)))
} else {
// Insert new
await db.insert(keys).values({
id: nanoid(),
userId: session.user.id,
provider,
value: 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(keys).where(and(eq(keys.userId, session.user.id), eq(keys.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 })
}
}
70 changes: 70 additions & 0 deletions app/api/auth/callback/vercel/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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(),
refreshToken: tokens.hasRefreshToken() ? tokens.refreshToken() : undefined,
})

if (!session) {
console.error('[Vercel Callback] Failed to create session')
return new Response('Failed to create session', { status: 500 })
}

// Note: Vercel tokens are already stored in users table by upsertUser() in createSession()

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