diff --git a/server.js b/server.js index b5f058b1..8ba96c38 100644 --- a/server.js +++ b/server.js @@ -782,6 +782,138 @@ function buildRouter() { res.json({ ok: true, userId: req.userId, authType: req.authType }); }); + // ── Auth: Google OAuth ─────────────────────────────────────────── + const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || ''; + const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || ''; + const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || ''; + const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || ''; + const OAUTH_BASE_URL = process.env.MAGIC_LINK_BASE_URL || 'http://localhost:3000'; + + // GET /api/auth/oauth/google — redirect to Google + r.get('/auth/oauth/google', (req, res) => { + const params = new URLSearchParams({ + client_id: GOOGLE_CLIENT_ID, + redirect_uri: `${process.env.API_PUBLIC_URL || 'https://api.soar-enrich.com'}/brain/api/auth/oauth/google/callback`, + response_type: 'code', + scope: 'openid email profile', + access_type: 'offline', + prompt: 'select_account', + }); + res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`); + }); + + // GET /api/auth/oauth/google/callback + r.get('/auth/oauth/google/callback', async (req, res) => { + const { code } = req.query; + if (!code) return res.redirect(`${OAUTH_BASE_URL}/login?error=no_code`); + try { + // Exchange 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: GOOGLE_CLIENT_ID, + client_secret: GOOGLE_CLIENT_SECRET, + redirect_uri: `${process.env.API_PUBLIC_URL || 'https://api.soar-enrich.com'}/brain/api/auth/oauth/google/callback`, + grant_type: 'authorization_code', + }), + }); + const tokenData = await tokenRes.json(); + if (!tokenData.access_token) throw new Error('No access token'); + + // Get user info + const userRes = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', { + headers: { Authorization: `Bearer ${tokenData.access_token}` }, + }); + const userInfo = await userRes.json(); + const email = userInfo.email?.toLowerCase(); + if (!email) throw new Error('No email from Google'); + + // Find or create user + const existing = await pool.query( + `SELECT id FROM users WHERE email = $1`, [email] + ); + let userId; + if (existing.rows.length > 0) { + userId = existing.rows[0].id; + } else { + const newUser = await pool.query( + `INSERT INTO users (email, created_at) VALUES ($1, NOW()) RETURNING id`, [email] + ); + userId = newUser.rows[0].id; + } + + const token = await createSessionJWT(userId); + res.redirect(`${OAUTH_BASE_URL}/auth/verify?token=${token}&type=oauth`); + } catch (e) { + console.error('[OAuth Google]', e); + res.redirect(`${OAUTH_BASE_URL}/login?error=google_failed`); + } + }); + + // ── Auth: GitHub OAuth ─────────────────────────────────────────── + + // GET /api/auth/oauth/github — redirect to GitHub + r.get('/auth/oauth/github', (req, res) => { + const params = new URLSearchParams({ + client_id: GITHUB_CLIENT_ID, + redirect_uri: `${process.env.API_PUBLIC_URL || 'https://api.soar-enrich.com'}/brain/api/auth/oauth/github/callback`, + scope: 'user:email', + }); + res.redirect(`https://github.com/login/oauth/authorize?${params}`); + }); + + // GET /api/auth/oauth/github/callback + r.get('/auth/oauth/github/callback', async (req, res) => { + const { code } = req.query; + if (!code) return res.redirect(`${OAUTH_BASE_URL}/login?error=no_code`); + try { + // Exchange code for token + const tokenRes = await fetch('https://github.com/login/oauth/access_token', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ + client_id: GITHUB_CLIENT_ID, + client_secret: GITHUB_CLIENT_SECRET, + code, + redirect_uri: `${process.env.API_PUBLIC_URL || 'https://api.soar-enrich.com'}/brain/api/auth/oauth/github/callback`, + }), + }); + const tokenData = await tokenRes.json(); + if (!tokenData.access_token) throw new Error('No access token'); + + // Get user emails + const emailRes = await fetch('https://api.github.com/user/emails', { + headers: { Authorization: `Bearer ${tokenData.access_token}`, 'User-Agent': 'Posimai' }, + }); + const emails = await emailRes.json(); + const primary = emails.find((e) => e.primary && e.verified); + const email = primary?.email?.toLowerCase(); + if (!email) throw new Error('No verified email from GitHub'); + + // Find or create user + const existing = await pool.query( + `SELECT id FROM users WHERE email = $1`, [email] + ); + let userId; + if (existing.rows.length > 0) { + userId = existing.rows[0].id; + } else { + const newUser = await pool.query( + `INSERT INTO users (email, created_at) VALUES ($1, NOW()) RETURNING id`, [email] + ); + userId = newUser.rows[0].id; + } + + const token = await createSessionJWT(userId); + res.redirect(`${OAUTH_BASE_URL}/auth/verify?token=${token}&type=oauth`); + } catch (e) { + console.error('[OAuth GitHub]', e); + res.redirect(`${OAUTH_BASE_URL}/login?error=github_failed`); + } + }); + // DELETE /api/auth/session — logout (revoke session in DB) r.delete('/auth/session', authMiddleware, async (req, res) => { try {