Files
ihaleist/server/auth.mjs
T

124 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = () => {
if (process.env.AUTH_SECRET) return process.env.AUTH_SECRET;
if (process.env.NODE_ENV === 'production') throw new Error('Üretimde AUTH_SECRET zorunludur.');
return process.env.GOOGLE_CLIENT_ID || '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('.');
let secret;
try { secret = AUTH_SECRET(); } catch { return null; }
const expected = b64url(crypto.createHmac('sha256', 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ış');
if (typeof p.sub !== 'string' || !p.sub.trim()) throw new Error('Google kullanıcı kimliği eksik');
if (typeof p.email !== 'string' || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(p.email)) throw new Error('Geçerli e-posta eksik');
const email = 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'],
};
}