/** * 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('

Error

No authorization code received. Please try again from the GOD Frame app.

'); } try { authLogger.debug('[Google Mobile] Exchanging code for token...'); // Use dynamic host-based redirect_uri that matches what was sent to Google in mobile-auth-url const redirectUri = getDynamicRedirectUri(req, googleOAuthConfig); authLogger.debug('[Google Mobile] 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; authLogger.debug('[Google Mobile] User email:', googleEmail); // Check if user exists let user = await dbGet('SELECT id, email, name, role FROM users WHERE email = ?', [googleEmail]); if (!user) { authLogger.debug('[Google Mobile] User not found:', googleEmail); return res.send(`

User Not Found

Email ${googleEmail} is not registered in GOD CRM.

Please contact administrator.

`); } authLogger.debug('[Google Mobile] User found:', googleEmail); // Create JWT token and refresh token const accessToken = createAccessToken(user); const refreshToken = createRefreshToken(user); // Set refresh token cookie (for completeness) res.cookie(REFRESH_COOKIE_NAME, refreshToken, getRefreshCookieOptions()); res.cookie(ACCESS_COOKIE_NAME, accessToken, getAccessCookieOptions()); // Determine app scheme from state (format: "mobile:" or just "") let appScheme = 'godframe'; if (state) { const decodedState = decodeURIComponent(state); appScheme = decodedState.startsWith('mobile:') ? decodedState.slice(7) : decodedState; } // Build deep link to redirect to the mobile app (include refresh token for persistent auth) const deepLink = `${appScheme}://auth/callback?token=${encodeURIComponent(accessToken)}&refresh_token=${encodeURIComponent(refreshToken)}&email=${encodeURIComponent(user.email)}&name=${encodeURIComponent(user.name)}`; authLogger.debug('[Google Mobile] Redirecting to app via deep link:', deepLink.substring(0, 80) + '...'); // Serve an HTML page that triggers the deep link redirect. const safeDeepLink = deepLink.replace(/"/g, '"').replace(/ Redirecting to GOD Frame...

Authentication Successful!

Opening GOD Frame...

Open GOD Frame

If the app doesn't open automatically, tap the button above.

`); } catch (error) { authLogger.error({ err: error }, '[Google Mobile] Error:', error.response?.data || error.message); return res.status(500).send(`

Authentication Error

${error.response?.data?.error_description || error.message}

Please try again from the GOD Frame app.

`); } }); // POST /api/v3/auth/google/config - Update Google OAuth config (owner only) router.post('/google/config', requireAuth, async (req, res) => { try { // Check if user is owner const user = await dbGet('SELECT role FROM users WHERE id = ?', [req.user.id]); if (user?.role !== 'owner') { return respondError(res, 403, 'FORBIDDEN', 'Only owner can configure Google OAuth'); } const { clientId, clientSecret, redirectUri, enabled } = req.body; let googleOAuthConfig = getConfig(); const newConfig = { clientId: clientId || googleOAuthConfig.clientId, clientSecret: clientSecret || googleOAuthConfig.clientSecret, redirectUri: redirectUri || googleOAuthConfig.redirectUri, enabled: enabled !== undefined ? enabled : googleOAuthConfig.enabled }; setGoogleOAuthConfig(newConfig); if (saveGoogleOAuthConfig(newConfig)) { return respondSuccess(res, { message: 'Google OAuth configuration updated', enabled: newConfig.enabled }); } else { return respondError(res, 500, 'SAVE_FAILED', 'Failed to save configuration'); } } catch (error) { authLogger.error({ err: error }, '[Google OAuth] Config update error:', error); return respondError(res, 500, 'CONFIG_UPDATE_FAILED', 'Failed to update Google OAuth configuration'); } }); // GET /api/v3/auth/google/config - Get Google OAuth config (owner only) // NOTE: This authenticated version is registered AFTER the public one above. // Express matches the first registered handler; the public GET /google/config (line ~30) // will always win. This mirrors the original auth.js behavior where the second // GET /google/config at line 1312 was effectively unreachable. // Kept here for parity — if you need an owner-only endpoint, use a different path. }