← back to Norma
app/api/auth/google/callback/route.ts
116 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { createSession, buildAuthCookie, hashPassword } from '@/lib/auth';
/**
* GET /api/auth/google/callback
* Receives the OAuth2 authorization code from Google, exchanges it for tokens,
* reads the user's profile, creates/updates a tier_credentials row, and sets the session cookie.
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
const error = searchParams.get('error');
if (error || !code) {
return NextResponse.redirect(new URL('/login?error=google_denied', request.url));
}
const clientId = process.env.GOOGLE_OAUTH_CLIENT_ID;
const clientSecret = process.env.GOOGLE_OAUTH_CLIENT_SECRET;
const redirectUri = process.env.GOOGLE_OAUTH_REDIRECT_URI || 'http://45.61.58.125:7400/api/auth/google/callback';
if (!clientId || !clientSecret) {
return NextResponse.redirect(new URL('/login?error=config', request.url));
}
try {
// Exchange authorization code for tokens
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
grant_type: 'authorization_code',
}),
});
if (!tokenRes.ok) {
console.error('[google-callback] Token exchange failed:', await tokenRes.text());
return NextResponse.redirect(new URL('/login?error=token_exchange', request.url));
}
const tokens = await tokenRes.json();
const accessToken = tokens.access_token;
// Fetch user profile from Google
const profileRes = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!profileRes.ok) {
console.error('[google-callback] Profile fetch failed:', await profileRes.text());
return NextResponse.redirect(new URL('/login?error=profile', request.url));
}
const profile = await profileRes.json();
const email: string = profile.email;
const displayName: string = profile.name || email.split('@')[0];
const googleId: string = profile.id;
if (!email) {
return NextResponse.redirect(new URL('/login?error=no_email', request.url));
}
// Check if user already exists by email (username = email for Google users)
const existing = await query(
'SELECT username, role, org_id, display_name, client_type FROM tier_credentials WHERE username = $1',
[email],
);
let role = 'pulse';
let orgId: string | null = null;
let clientType: string | null = null;
if (existing.rows.length > 0) {
// Existing user — use their role
role = existing.rows[0].role;
orgId = existing.rows[0].org_id;
clientType = existing.rows[0].client_type;
// Update display name and last login
await query(
'UPDATE tier_credentials SET display_name = $1, updated_at = NOW() WHERE username = $2',
[displayName, email],
);
} else {
// New user — create as pulse (public user) by default
// Generate a random password hash (user won't use password login)
const randomPw = await hashPassword(`google_${googleId}_${Date.now()}`);
await query(
`INSERT INTO tier_credentials (username, password_hash, role, display_name, client_type)
VALUES ($1, $2, $3, $4, NULL)
ON CONFLICT (username) DO NOTHING`,
[email, randomPw, 'pulse', displayName],
);
}
// Create session
const token = createSession(email, role, orgId);
const cookieValue = buildAuthCookie(token);
// Redirect to appropriate page
const target = role === 'pulse' ? '/pulse' : '/';
const response = NextResponse.redirect(new URL(target, request.url));
response.headers.set('Set-Cookie', cookieValue);
return response;
} catch (err) {
console.error('[google-callback] Error:', (err as Error).message);
return NextResponse.redirect(new URL('/login?error=server', request.url));
}
}