const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const cookieParser = require('cookie-parser'); const axios = require('axios'); const { OAuth2Client } = require('google-auth-library'); const { v4: uuidv4 } = require('uuid'); require('dotenv').config(); // ========================================== // نظام حماية الخادم من الانهيار (يمنع دوامة Restarting) // ========================================== process.on('uncaughtException', (err) => { console.error('CRITICAL ERROR (Uncaught Exception):', err); }); process.on('unhandledRejection', (reason, promise) => { console.error('CRITICAL ERROR (Unhandled Rejection):', reason); }); const { db, initDB } = require('./db'); const { sendWelcomeEmail } = require('./email'); const app = express(); const PORT = process.env.PORT || 7860; // ========================================== // 1. نظام السجلات المتقدم (In-Memory Logs) // ========================================== let serverLogs = []; const MAX_LOGS = 5000; function addLog(level, category, message, details = '') { const logEntry = { id: uuidv4(), timestamp: new Date().toISOString(), type: level.toLowerCase(), // info, warn, error, security, event, request category: category, message: message, metadata: typeof details === 'object' ? details : { info: details } }; serverLogs.unshift(logEntry); if (serverLogs.length > MAX_LOGS) serverLogs.pop(); console.log(`[${logEntry.timestamp}] [${logEntry.type.toUpperCase()}] [${logEntry.category}] - ${logEntry.message}`); } function logSecurityEvent(type, ip, userAgent, status, details = '') { const level = status === 'FAILED' ? 'error' : (status === 'SUCCESS' ? 'info' : 'warn'); addLog(level, 'auth', `[${type}] [${status}]`, { ip, userAgent, details }); } // ========================================== // 2. إعدادات الحماية والـ Middlewares // ========================================== app.use(helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false })); app.use(cors({ origin: function(origin, callback) { callback(null, true); }, credentials: true })); app.use(express.json()); app.use(cookieParser()); // Request Logger Middleware app.use((req, res, next) => { const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; let type = res.statusCode >= 500 ? 'error' : (res.statusCode >= 400 ? 'warn' : 'info'); addLog(type, 'request', `${req.method} ${req.path} → ${res.statusCode} (${duration}ms)`, { ip, method: req.method, path: req.path }); }); next(); }); const googleClient = new OAuth2Client( process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET ); // ========================================== // 3. مسارات نظام السجلات (Logs API) // ========================================== function adminAuth(req, res, next) { const b64auth = (req.headers.authorization || '').split(' ')[1] || ''; const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':'); // يجب إضافة ADMIN_USER و ADMIN_PASS في متغيرات البيئة const adminUser = process.env.ADMIN_USER || 'admin'; const adminPass = process.env.ADMIN_PASS || 'admin'; if (login && password && login === adminUser && password === adminPass) { return next(); } addLog('warn', 'security', 'محاولة وصول غير مصرح بها لصفحة السجلات', { ip: req.ip }); res.set('WWW-Authenticate', 'Basic realm="401"'); res.status(401).send('Authentication required.'); } app.get('/api/logs', adminAuth, (req, res) => { res.json({ logs: serverLogs, count: serverLogs.length }); }); app.post('/api/logs/clear', adminAuth, (req, res) => { serverLogs = []; addLog('info', 'system', 'تم مسح جميع السجلات بواسطة المسؤول', { ip: req.ip }); res.json({ success: true }); }); app.post('/api/logs', (req, res) => { const { type, category, message, metadata } = req.body; const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; addLog(type || 'event', category || 'frontend', message || 'Client Event', { ...metadata, ip }); res.json({ success: true }); }); // ========================================== // 4. مسارات المصادقة (Authentication) // ========================================== // أ. توليد رابط جوجل (لزر جوجل المخصص - Redirect Flow) app.post('/api/auth/google-url', async (req, res) => { const { captchaToken, marketing, redirectUri } = req.body; const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; const userAgent = req.headers['user-agent']; if (!captchaToken || !redirectUri) { logSecurityEvent('hCaptcha', ip, userAgent, 'FAILED', 'Missing parameters'); return res.status(400).json({ message: "Missing parameters" }); } try { const verifyRes = await axios.post('https://hcaptcha.com/siteverify', new URLSearchParams({ secret: process.env.HCAPTCHA_SECRET_KEY, response: captchaToken })); if (!verifyRes.data.success) { logSecurityEvent('hCaptcha', ip, userAgent, 'FAILED', 'Invalid token'); return res.status(403).json({ message: "Captcha verification failed" }); } logSecurityEvent('hCaptcha', ip, userAgent, 'SUCCESS'); const stateObj = { marketing: !!marketing, csrf: uuidv4() }; const stateStr = Buffer.from(JSON.stringify(stateObj)).toString('base64'); const url = googleClient.generateAuthUrl({ access_type: 'offline', scope: ['email', 'profile'], state: stateStr, redirect_uri: redirectUri // الرابط الذي سيظهر في جوجل (skydata.bond/home.html) }); res.json({ url }); } catch (err) { addLog('error', 'auth', 'Google URL Generation Failed', { error: err.message }); res.status(500).json({ message: "Server error" }); } }); // ب. استقبال الكود من home.html وإنشاء الجلسة app.post('/api/auth/google/callback-ajax', async (req, res) => { const { code, state, redirectUri } = req.body; const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; const userAgent = req.headers['user-agent']; if (!code || !redirectUri) { return res.status(400).json({ message: "Missing code or redirectUri" }); } try { const { tokens } = await googleClient.getToken({ code, redirect_uri: redirectUri }); const ticket = await googleClient.verifyIdToken({ idToken: tokens.id_token, audience: process.env.GOOGLE_CLIENT_ID }); const payload = ticket.getPayload(); const email = payload.email; const name = payload.name || email.split('@')[0]; const picture = payload.picture; let marketing = false; if (state) { try { const stateObj = JSON.parse(Buffer.from(state, 'base64').toString('utf-8')); marketing = stateObj.marketing; } catch(e) {} } let userRes = await db.execute({ sql: "SELECT * FROM users WHERE email = ?", args: [email] }); let userId; if (userRes.rows.length === 0) { const username = email.split('@')[0] + Math.floor(Math.random() * 1000); const insertRes = await db.execute({ sql: `INSERT INTO users (username, display_name, email, profile, marketing_mail, last_login_ip) VALUES (?, ?, ?, ?, ?, ?) RETURNING user_id`, args: [username, name, email, picture, marketing ? 1 : 0, ip] }); userId = insertRes.rows[0].user_id; if (marketing) sendWelcomeEmail(email, name); } else { userId = userRes.rows[0].user_id; await db.execute({ sql: "UPDATE users SET login_count = login_count + 1, last_login_ip = ? WHERE user_id = ?", args: [ip, userId] }); } logSecurityEvent('Login', ip, userAgent, 'SUCCESS', `User: ${email}`); const sessionId = uuidv4(); const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); await db.execute({ sql: `INSERT INTO sessions (session_id, user_id, expires_at, ip, user_agent) VALUES (?, ?, ?, ?, ?)`, args: [sessionId, userId, expiresAt, ip, userAgent] }); res.cookie('skydata_session', sessionId, { httpOnly: true, secure: true, sameSite: 'None', maxAge: 7 * 24 * 60 * 60 * 1000 }); const finalUserRes = await db.execute({ sql: "SELECT username, display_name, email, profile, lv, power, xp, coins, gems, hp FROM users WHERE user_id = ?", args: [userId] }); res.json({ success: true, user: finalUserRes.rows[0] }); } catch (err) { logSecurityEvent('Login', ip, userAgent, 'FAILED', err.message); res.status(500).json({ message: "Authentication failed: " + err.message }); } }); // ========================================== // 5. إدارة الجلسات (Session Management) // ========================================== async function requireAuth(req, res, next) { const sessionId = req.cookies.skydata_session; if (!sessionId) return res.status(401).json({ message: "Unauthorized" }); try { const sessionRes = await db.execute({ sql: "SELECT * FROM sessions WHERE session_id = ? AND is_active = 1", args: [sessionId] }); if (sessionRes.rows.length === 0) { res.clearCookie('skydata_session'); return res.status(401).json({ message: "Invalid session" }); } const session = sessionRes.rows[0]; const now = new Date(); const expiresAt = new Date(session.expires_at); if (now > expiresAt) { await db.execute({ sql: "UPDATE sessions SET is_active = 0 WHERE session_id = ?", args: [sessionId] }); res.clearCookie('skydata_session'); return res.status(401).json({ message: "Session expired" }); } const createdAt = new Date(session.created_at); const diffMins = (now - createdAt) / (1000 * 60); if (diffMins > 15) { const newSessionId = uuidv4(); await db.execute({ sql: "UPDATE sessions SET is_active = 0 WHERE session_id = ?", args: [sessionId] }); await db.execute({ sql: `INSERT INTO sessions (session_id, user_id, expires_at, ip, user_agent) VALUES (?, ?, ?, ?, ?)`, args: [newSessionId, session.user_id, session.expires_at, session.ip, session.user_agent] }); res.cookie('skydata_session', newSessionId, { httpOnly: true, secure: true, sameSite: 'None', expires: expiresAt }); } else { await db.execute({ sql: "UPDATE sessions SET last_activity = CURRENT_TIMESTAMP WHERE session_id = ?", args: [sessionId] }); } req.userId = session.user_id; next(); } catch (err) { addLog('error', 'system', 'Session Verification Error', { error: err.message }); res.status(500).json({ message: "Server error" }); } } app.get('/api/auth/me', requireAuth, async (req, res) => { try { const userRes = await db.execute({ sql: "SELECT username, display_name, email, profile, lv, power, xp, coins, gems, hp FROM users WHERE user_id = ?", args: [req.userId] }); res.json(userRes.rows[0]); } catch (err) { res.status(500).json({ message: "Server error" }); } }); app.post('/api/auth/settings', requireAuth, async (req, res) => { const { sessionDays } = req.body; const days = parseInt(sessionDays) || 7; try { const expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString(); await db.execute({ sql: "UPDATE sessions SET expires_at = ? WHERE session_id = ?", args: [expiresAt, req.cookies.skydata_session] }); res.cookie('skydata_session', req.cookies.skydata_session, { httpOnly: true, secure: true, sameSite: 'None', maxAge: days * 24 * 60 * 60 * 1000 }); res.json({ success: true }); } catch (err) { res.status(500).json({ message: "Server error" }); } }); app.post('/api/auth/logout', requireAuth, async (req, res) => { try { await db.execute({ sql: "UPDATE sessions SET is_active = 0 WHERE session_id = ?", args: [req.cookies.skydata_session] }); res.clearCookie('skydata_session', { httpOnly: true, secure: true, sameSite: 'None' }); res.json({ success: true }); } catch (err) { res.status(500).json({ message: "Server error" }); } }); // ========================================== // 6. بدء تشغيل الخادم // ========================================== initDB().then(() => { app.listen(PORT, '0.0.0.0', () => { console.log(`✅ Server running on port ${PORT}`); addLog('info', 'system', `Server started o