J / server.js
Skydata001's picture
Create server.js
87d0a95 verified
Raw
History Blame Contribute Delete
9.83 kB
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}`);
});