/** * Google OAuth routes: /google/config, /google/auth-url, /google/token, * /google/callback, /google/mobile-auth-url, /google/mobile-callback */ import axios from 'axios'; import { respondSuccess, respondError, REFRESH_COOKIE_NAME, ACCESS_COOKIE_NAME, getRefreshCookieOptions, getAccessCookieOptions, createAccessToken, createRefreshToken, requireAuth, getDynamicRedirectUri, saveGoogleOAuthConfig, googleOAuthConfig as _googleOAuthConfig, setGoogleOAuthConfig, dbGet, authLogger } from './authShared.js'; // Local reference that stays in sync via the shared module's getter function getConfig() { // Re-import to get the current mutable value // We read from the shared module each time so POST /google/config updates are visible return _googleOAuthConfig; } /** * @param {import('express').Router} router */ export default function registerGoogleOAuthRoutes(router) { // GET /api/v3/auth/google/config - Get Google OAuth config (public info only) router.get('/google/config', (req, res) => { const googleOAuthConfig = getConfig(); return respondSuccess(res, { clientId: googleOAuthConfig.clientId, redirectUri: getDynamicRedirectUri(req, googleOAuthConfig), enabled: googleOAuthConfig.enabled, hasClientSecret: !!googleOAuthConfig.clientSecret }); }); // GET /api/v3/auth/google/auth-url - Get Google OAuth authorization URL router.get('/google/auth-url', (req, res) => { const googleOAuthConfig = getConfig(); if (!googleOAuthConfig.enabled || !googleOAuthConfig.clientId) { return respondError(res, 400, 'GOOGLE_OAUTH_NOT_CONFIGURED', 'Google OAuth is not configured'); } // Support custom redirect_uri for desktop apps (localhost), otherwise use dynamic host-based URI const redirectUri = req.query.redirect_uri || getDynamicRedirectUri(req, googleOAuthConfig); const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` + `client_id=${googleOAuthConfig.clientId}&` + `redirect_uri=${encodeURIComponent(redirectUri)}&` + `response_type=code&` + `scope=openid%20profile%20email&` + `prompt=select_account&` + `access_type=offline`; return respondSuccess(res, { url: authUrl, redirectUri }); }); // POST /api/v3/auth/google/token - Login with Google access_token (for desktop apps) // Desktop app handles OAuth flow and token exchange, sends us the Google access_token router.post('/google/token', async (req, res) => { const { access_token } = req.body; if (!access_token) { return respondError(res, 400, 'NO_TOKEN', 'Google access_token is required'); } try { authLogger.debug('[Google Token] Verifying access_token...'); // Get user info from Google using the access_token const userResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', { headers: { Authorization: `Bearer ${access_token}` } }); const googleEmail = userResponse.data.email; const googleName = userResponse.data.name || googleEmail.split('@')[0]; authLogger.debug('[Google Token] User email:', googleEmail); // Check if user exists let user = await dbGet('SELECT id, email, name, role FROM users WHERE email = ?', [googleEmail]); if (!user) { // User not found - return error (no auto-registration) authLogger.debug('[Google Token] User not found:', googleEmail); return respondError(res, 401, 'USER_NOT_FOUND', `User with email ${googleEmail} is not registered in the system. Please contact administrator.`); } authLogger.debug('[Google Token] User found:', googleEmail); // Create tokens const accessToken = createAccessToken(user); const refreshToken = createRefreshToken(user); // Set refresh token cookie res.cookie(REFRESH_COOKIE_NAME, refreshToken, getRefreshCookieOptions()); res.cookie(ACCESS_COOKIE_NAME, accessToken, getAccessCookieOptions()); // Include refreshToken in body for mobile clients (they can't use httpOnly cookies) return respondSuccess(res, { user: { id: user.id, email: user.email, name: user.name, role: user.role }, accessToken, refreshToken }); } catch (error) { authLogger.error({ err: error }, '[Google Token] Error:', error.response?.data || error.message); return respondError(res, 500, 'GOOGLE_AUTH_FAILED', 'Google authentication failed', error.response?.data?.error_description || error.message); } }); // POST /api/v3/auth/google/callback - Exchange code for token and login router.post('/google/callback', async (req, res) => { const googleOAuthConfig = getConfig(); const { code, redirect_uri } = req.body; if (!code) { return respondError(res, 400, 'NO_CODE', 'Authorization code is required'); } try { authLogger.debug(' Exchanging code for token...'); // Use provided redirect_uri or dynamic host-based URI (for desktop apps using localhost) const redirectUri = redirect_uri || getDynamicRedirectUri(req, googleOAuthConfig); authLogger.debug(' Using redirect_uri:', redirectUri); // Exchange code for token const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', { code, client_id: googleOAuthConfig.clientId, client_secret: googleOAuthConfig.clientSecret, redirect_uri: redirectUri, grant_type: 'authorization_code' }); const { access_token } = tokenResponse.data; // Get user info from Google const userResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', { headers: { Authorization: `Bearer ${access_token}` } }); const googleEmail = userResponse.data.email; const googleName = userResponse.data.name || googleEmail.split('@')[0]; authLogger.debug(' User email:', googleEmail); // Check if user exists let user = await dbGet('SELECT id, email, name, role FROM users WHERE email = ?', [googleEmail]); if (!user) { // User not found - return error (no auto-registration) authLogger.debug(' User not found:', googleEmail); return respondError(res, 401, 'USER_NOT_FOUND', `User with email ${googleEmail} is not registered in the system. Please contact administrator.`); } authLogger.debug(' User found:', googleEmail); // Create tokens const accessToken = createAccessToken(user); const refreshToken = createRefreshToken(user); // Set refresh token cookie res.cookie(REFRESH_COOKIE_NAME, refreshToken, getRefreshCookieOptions()); res.cookie(ACCESS_COOKIE_NAME, accessToken, getAccessCookieOptions()); // Include refreshToken in body for mobile clients return respondSuccess(res, { user: { id: user.id, email: user.email, name: user.name, role: user.role }, accessToken, refreshToken }); } catch (error) { authLogger.error({ err: error }, '[Google OAuth] Error:', error.response?.data || error.message); return respondError(res, 500, 'GOOGLE_AUTH_FAILED', 'Google authentication failed', error.response?.data?.error_description || error.message); } }); // GET /api/v3/auth/google/mobile-auth-url - Get Google OAuth URL for mobile apps router.get('/google/mobile-auth-url', (req, res) => { const googleOAuthConfig = getConfig(); if (!googleOAuthConfig.enabled || !googleOAuthConfig.clientId) { return respondError(res, 400, 'GOOGLE_OAUTH_NOT_CONFIGURED', 'Google OAuth is not configured'); } // Use dynamic host-based redirect URI (same as web app) const redirectUri = getDynamicRedirectUri(req, googleOAuthConfig); // App scheme passed as query parameter const appScheme = req.query.app_scheme || 'godframe'; // Encode mobile flag in state so server.js can detect and forward to mobile-callback const state = `mobile:${appScheme}`; const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` + `client_id=${googleOAuthConfig.clientId}&` + `redirect_uri=${encodeURIComponent(redirectUri)}&` + `response_type=code&` + `scope=openid%20profile%20email&` + `prompt=select_account&` + `state=${encodeURIComponent(state)}&` + `access_type=offline`; return respondSuccess(res, { url: authUrl, redirectUri }); }); // GET /api/v3/auth/google/mobile-callback - Handle Google OAuth callback for mobile apps router.get('/google/mobile-callback', async (req, res) => { const googleOAuthConfig = getConfig(); const { code, state } = req.query; if (!code) { return res.status(400).send('
No authorization code received. Please try again from the GOD Frame app.
Email ${googleEmail} is not registered in GOD CRM.
Please contact administrator.
Opening GOD Frame...
Open GOD FrameIf the app doesn't open automatically, tap the button above.
${error.response?.data?.error_description || error.message}
Please try again from the GOD Frame app.