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(); const { db, initDB } = require('./db'); const { sendWelcomeEmail } = require('./email'); const app = express(); const PORT = process.env.PORT || 7860; // Security Middlewares app.use(helmet()); app.use(cors({ origin: ['https://skydata.bond', 'https://www.skydata.bond', 'http://localhost:3000'], credentials: true })); app.use(express.json()); app.use(cookieParser()); // Google Auth Client const googleClient = new OAuth2Client( process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, `${process.env.BACKEND_URL}/api/auth/google/callback` ); // Initialize DB initDB(); // --- Helper: Log Security Events --- function logSecurityEvent(type, ip, userAgent, status, details = '') { const time = new Date().toISOString(); console.log(`[SECURITY] [${time}] [${ip}] [${type}] [${status}] - ${details} | UA: ${userAgent}`); } // --- 1. Generate Google Auth URL (After hCaptcha verification) --- app.post('/api/auth/google-url', async (req, res) => { const { captchaToken, marketing } = req.body; const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; const userAgent = req.headers['user-agent']; if (!captchaToken) { logSecurityEvent('hCaptcha', ip, userAgent, 'FAILED', 'No token provided'); return res.status(400).json({ message: "Captcha required" }); } try { // Verify hCaptcha with server 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'); // Generate state token to pass marketing preference securely 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 }); res.json({ url }); } catch (err) { console.error(err); res.status(500).json({ message: "Server error" }); } }); // --- 2. Google Callback & Session Creation --- app.get('/api/auth/google/callback', async (req, res) => { const { code, state } = req.query; const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; const userAgent = req.headers['user-agent']; try { const { tokens } = await googleClient.getToken(code); 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; const picture = payload.picture; // Decode state let marketing = false; if (state) { const stateObj = JSON.parse(Buffer.from(state, 'base64').toString('utf-8')); marketing = stateObj.marketing; } // Check if user exists let userRes = await db.execute({ sql: "SELECT * FROM users WHERE email = ?", args: [email] }); let userId; let isNewUser = false; if (userRes.rows.length === 0) { // Create new user 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; isNewUser = true; if (marketing) { sendWelcomeEmail(email, name); } } else { userId = userRes.rows[0].user_id; // Update login info 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}`); // Create Secure Session (Default 7 days) 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] }); // Set HttpOnly Cookie res.cookie('skydata_session', sessionId, { httpOnly: true, secure: true, // Must be true for SameSite=None sameSite: 'None', // Required for cross-domain (Vercel -> HF) maxAge: 7 * 24 * 60 * 60 * 1000 }); // Redirect back to frontend with success flag res.redirect('https://skydata.bond/?auth=success'); } catch (err) { logSecurityEvent('Login', ip, userAgent, 'FAILED', err.message); res.redirect('https://skydata.bond/?auth=failed'); } }); // --- Middleware: Verify Session & Rotate every 15 mins --- 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" }); } // Session Rotation (Every 15 mins) const createdAt = new Date(session.created_at); const diffMins = (now - createdAt) / (1000 * 60); if (diffMins > 15) { const newSessionId = uuidv4(); // Invalidate old, create new with same expiration 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 { // Update last activity await db.execute({ sql: "UPDATE sessions SET last_activity = CURRENT_TIMESTAMP WHERE session_id = ?", args: [sessionId] }); } req.userId = session.user_id; next(); } catch (err) { res.status(500).json({ message: "Server error" }); } } // --- 3. Get User Profile --- app.get('/api/auth/me', requireAuth, async (req, res) => { try { const userRes = await db.execute({ sql: "SELECT username, display_name, email, profile, lv, power FROM users WHERE user_id = ?", args: [req.userId] }); res.json(userRes.rows[0]); } catch (err) { res.status(500).json({ message: "Server error" }); } }); // --- 4. Update Session Settings --- 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" }); } }); // --- 5. Logout --- 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" }); } }); app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });