HonzysClawdbot
fix(session-cookie): migrate to __Host- prefix for secure contexts (#294)
c9c4e2a unverified
import { randomBytes } from 'crypto'
import { NextRequest, NextResponse } from 'next/server'
import { createSession } from '@/lib/auth'
import { getDatabase, logAuditEvent } from '@/lib/db'
import { verifyGoogleIdToken } from '@/lib/google-auth'
import { getMcSessionCookieName, getMcSessionCookieOptions, isRequestSecure } from '@/lib/session-cookie'
import { loginLimiter } from '@/lib/rate-limit'
function upsertAccessRequest(input: {
email: string
providerUserId: string
displayName: string
avatarUrl?: string
}) {
const db = getDatabase()
db.prepare(`
INSERT INTO access_requests (provider, email, provider_user_id, display_name, avatar_url, status, attempt_count, requested_at, last_attempt_at)
VALUES ('google', ?, ?, ?, ?, 'pending', 1, (unixepoch()), (unixepoch()))
ON CONFLICT(email, provider) DO UPDATE SET
provider_user_id = excluded.provider_user_id,
display_name = excluded.display_name,
avatar_url = excluded.avatar_url,
status = 'pending',
attempt_count = access_requests.attempt_count + 1,
last_attempt_at = (unixepoch())
`).run(input.email.toLowerCase(), input.providerUserId, input.displayName, input.avatarUrl || null)
}
export async function POST(request: NextRequest) {
const rateCheck = loginLimiter(request)
if (rateCheck) return rateCheck
try {
const body = await request.json().catch(() => ({}))
const credential = String(body?.credential || '')
const profile = await verifyGoogleIdToken(credential)
const db = getDatabase()
const email = String(profile.email || '').toLowerCase().trim()
const sub = String(profile.sub || '').trim()
const displayName = String(profile.name || email.split('@')[0] || 'Google User').trim()
const avatar = profile.picture ? String(profile.picture) : null
const row = db.prepare(`
SELECT u.id, u.username, u.display_name, u.role, u.provider, u.email, u.avatar_url, u.is_approved,
u.created_at, u.updated_at, u.last_login_at, u.workspace_id, COALESCE(w.tenant_id, 1) as tenant_id
FROM users u
LEFT JOIN workspaces w ON w.id = u.workspace_id
WHERE (provider = 'google' AND provider_user_id = ?) OR lower(email) = ?
ORDER BY id ASC
LIMIT 1
`).get(sub, email) as any
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
const userAgent = request.headers.get('user-agent') || undefined
if (!row || Number(row.is_approved ?? 1) !== 1) {
upsertAccessRequest({
email,
providerUserId: sub,
displayName,
avatarUrl: avatar || undefined,
})
logAuditEvent({
action: 'google_login_pending_approval',
actor: email,
detail: { email, sub },
ip_address: ipAddress,
user_agent: userAgent,
})
return NextResponse.json(
{ error: 'Access request pending admin approval', code: 'PENDING_APPROVAL' },
{ status: 403 }
)
}
db.prepare(`
UPDATE users
SET provider = 'google', provider_user_id = ?, email = ?, avatar_url = COALESCE(?, avatar_url), updated_at = (unixepoch())
WHERE id = ?
`).run(sub, email, avatar, row.id)
const { token, expiresAt } = createSession(row.id, ipAddress, userAgent, row.workspace_id ?? 1)
logAuditEvent({ action: 'login_google', actor: row.username, actor_id: row.id, ip_address: ipAddress, user_agent: userAgent })
const response = NextResponse.json({
user: {
id: row.id,
username: row.username,
display_name: row.display_name,
role: row.role,
provider: 'google',
email,
avatar_url: avatar,
workspace_id: row.workspace_id ?? 1,
tenant_id: row.tenant_id ?? 1,
},
})
const isSecureRequest = isRequestSecure(request)
const cookieName = getMcSessionCookieName(isSecureRequest)
response.cookies.set(cookieName, token, {
...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000), isSecureRequest }),
})
return response
} catch (error: any) {
return NextResponse.json({ error: error?.message || 'Google login failed' }, { status: 400 })
}
}