J
File size: 9,825 Bytes
87d0a95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
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}`);
});