/** * Google ile Giriş — backend doğrulama (ek bağımlılık yok). * GIS ID-token akışı: frontend Google'dan ID token alır, buraya gönderir; * biz oauth2.googleapis.com/tokeninfo ile doğrular, aud=GOOGLE_CLIENT_ID + * email_verified kontrol eder, HMAC imzalı kısa bir uygulama token'ı veririz. * Admin: ADMIN_EMAILS listesindeki e-postalar. */ import crypto from 'node:crypto'; import https from 'node:https'; const CLIENT_ID = () => process.env.GOOGLE_CLIENT_ID || null; const AUTH_SECRET = () => { const configured = process.env.AUTH_SECRET || process.env.GOOGLE_CLIENT_ID; if (configured) return configured; if (process.env.NODE_ENV === 'production') throw new Error('Üretimde AUTH_SECRET zorunludur.'); return 'ihaleist-dev-secret'; }; const TOKEN_TTL_SEC = 30 * 24 * 60 * 60; // 30 gün export function adminEmails() { return (process.env.ADMIN_EMAILS || '') .split(',') .map((e) => e.trim().toLowerCase()) .filter(Boolean); } export function isAdminEmail(email) { return adminEmails().includes(String(email || '').toLowerCase()); } // --- Uygulama token'ı (HMAC imzalı, kompakt) --- function b64url(buf) { return Buffer.from(buf).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } function b64urlJson(obj) { return b64url(JSON.stringify(obj)); } export function signToken(payload) { const body = { ...payload, exp: Math.floor(Date.now() / 1000) + TOKEN_TTL_SEC }; const data = b64urlJson(body); const sig = b64url(crypto.createHmac('sha256', AUTH_SECRET()).update(data).digest()); return `${data}.${sig}`; } export function verifyToken(token) { if (!token || typeof token !== 'string' || !token.includes('.')) return null; const [data, sig] = token.split('.'); const expected = b64url(crypto.createHmac('sha256', AUTH_SECRET()).update(data).digest()); if ( sig.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)) ) { return null; } try { const body = JSON.parse(Buffer.from(data.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString()); if (body.exp && body.exp < Math.floor(Date.now() / 1000)) return null; return body; } catch { return null; } } // --- Google ID token doğrulama (tokeninfo) --- function fetchJson(url) { return new Promise((resolve, reject) => { https .get(url, { timeout: 15000 }, (res) => { const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => { const text = Buffer.concat(chunks).toString('utf8'); if (res.statusCode !== 200) return reject(new Error(`tokeninfo HTTP ${res.statusCode}`)); try { resolve(JSON.parse(text)); } catch { reject(new Error('tokeninfo geçersiz JSON')); } }); }) .on('timeout', function () { this.destroy(new Error('tokeninfo zaman aşımı')); }) .on('error', reject); }); } export async function loginWithGoogle(credential) { const clientId = CLIENT_ID(); if (!clientId) throw new Error('Google girişi kapalı (GOOGLE_CLIENT_ID yok)'); if (!credential) throw new Error('credential yok'); const p = await fetchJson( `https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(credential)}`, ); if (p.aud !== clientId) throw new Error('aud eşleşmiyor'); if (p.email_verified !== 'true' && p.email_verified !== true) throw new Error('e-posta doğrulanmamış'); const email = String(p.email).toLowerCase(); const isAdmin = isAdminEmail(email); const user = { email, name: p.name || p.given_name || email.split('@')[0], picture: p.picture || null, isAdmin, roles: isAdmin ? ['USER', 'ADMIN'] : ['USER'], }; const token = signToken({ email: user.email, name: user.name, isAdmin }); return { token, user }; } export function userFromAuthHeader(authHeader) { const m = /^Bearer\s+(.+)$/i.exec(authHeader || ''); if (!m) return null; const claims = verifyToken(m[1]); if (!claims) return null; return { email: claims.email, name: claims.name, isAdmin: Boolean(claims.isAdmin), roles: claims.isAdmin ? ['USER', 'ADMIN'] : ['USER'], }; }