- server/auth.mjs: ID token'ı oauth2.googleapis.com/tokeninfo ile doğrula (ek bağımlılık yok), HMAC imzalı app token, ADMIN_EMAILS ile admin - POST /api/auth/google, GET /api/auth/google/config, GET /api/auth/me - LoginPage: GIS butonlu landing/giriş; giriş yapılmadan uygulama açılmaz - Header çıkış butonu artık çalışıyor; kullanıcı adı/avatar Google'dan - AI Model Ayarları sekmesi sadece admin'e görünür - Env: GOOGLE_CLIENT_ID + ADMIN_EMAILS (Coolify'dan)
117 lines
4.0 KiB
JavaScript
117 lines
4.0 KiB
JavaScript
/**
|
||
* 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 = () =>
|
||
process.env.AUTH_SECRET || 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('.');
|
||
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'],
|
||
};
|
||
}
|