Spaces:
Paused
Paused
| 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}`); | |
| }); |