← 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));
  }
}