require('dotenv').config(); const express = require('express'); const axios = require('axios'); const https = require('https'); const path = require('path'); const session = require('express-session'); const db = require('./db'); const app = express(); const PORT = 80; // Admin Configuration const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'change-this-admin-password'; // Telegram Configuration const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID; async function sendTelegramNotification(text) { if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) return; try { await axios.post( `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, { chat_id: TELEGRAM_CHAT_ID, text: text, parse_mode: 'HTML' }, { timeout: 15000 } ); } catch (err) { console.error('Telegram notification error:', err.message, err.response?.status ? `(HTTP ${err.response.status})` : ''); } } // PayPal Configuration const PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID; const PAYPAL_CLIENT_SECRET = process.env.PAYPAL_CLIENT_SECRET; const PAYPAL_MODE = (process.env.PAYPAL_MODE || 'sandbox').toLowerCase(); const PAYPAL_BASE_URL = PAYPAL_MODE === 'live' ? 'https://api-m.paypal.com' : 'https://api-m.sandbox.paypal.com'; async function getPayPalAccessToken() { const auth = Buffer.from(`${PAYPAL_CLIENT_ID}:${PAYPAL_CLIENT_SECRET}`).toString('base64'); const res = await axios.post( `${PAYPAL_BASE_URL}/v1/oauth2/token`, 'grant_type=client_credentials', { headers: { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/x-www-form-urlencoded' } } ); return res.data.access_token; } // UniFi Controller Configuration (from .env) const UNIFI_CONTROLLER = process.env.UNIFI_CONTROLLER || 'https://192.168.1.1'; const UNIFI_API_KEY = process.env.UNIFI_API_KEY; const UNIFI_SITE = process.env.UNIFI_SITE || 'default'; // Create axios instance that ignores SSL certificate errors (for self-signed certs) const unifiAgent = new https.Agent({ rejectUnauthorized: false }); const unifiApi = axios.create({ httpsAgent: unifiAgent, headers: { 'X-API-KEY': UNIFI_API_KEY, 'Content-Type': 'application/json' } }); // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(express.static('public')); app.use(session({ secret: process.env.SESSION_SECRET || 'change-this-secret', resave: false, saveUninitialized: true, cookie: { secure: false } // Set to true if using HTTPS })); // Admin middleware function requireAdmin(req, res, next) { if (req.session && req.session.admin) return next(); if (req.xhr || req.headers.accept?.includes('application/json')) { return res.status(401).json({ error: 'Unauthorized' }); } res.redirect('/admin'); } // Portal handler function function handlePortal(req, res) { // Capture UniFi redirect parameters const { id, ap, t, url, ssid } = req.query; if (!id) { return res.send('Missing required parameters. Please connect to the WiFi network.'); } // Store guest info in session req.session.guestMac = id; req.session.apMac = ap; req.session.redirectUrl = url || 'https://google.com'; req.session.ssid = ssid; res.sendFile(path.join(__dirname, 'public', 'index.html')); } // Main portal page app.get('/', handlePortal); // UniFi Guest Portal paths (UniFi redirects to /guest/s//) app.get('/guest/s/:site', handlePortal); app.get('/guest/s/:site/', handlePortal); // Authorize guest endpoint app.post('/authorize', async (req, res) => { const guestMac = req.session.guestMac; const redirectUrl = req.session.redirectUrl; const { code } = req.body || {}; if (!guestMac) { return res.status(400).json({ success: false, message: 'Session expired. Please reconnect to WiFi.' }); } const codeEntry = db.findAndValidateCode(code); if (!codeEntry) { return res.status(400).json({ success: false, message: 'Invalid or expired code.' }); } try { // Authorize the guest device using API key console.log(`Authorizing device: ${guestMac}`); const authorizeResponse = await unifiApi.post( `${UNIFI_CONTROLLER}/proxy/network/api/s/${UNIFI_SITE}/cmd/stamgr`, { cmd: 'authorize-guest', mac: guestMac.toLowerCase(), minutes: parseInt(process.env.ACCESS_DURATION) || 1440 } ); console.log('Authorization successful!', authorizeResponse.data); db.incrementCodeUseCount(code); res.json({ success: true, message: 'Connected! Redirecting...', redirectUrl: redirectUrl }); } catch (error) { console.error('Authorization error:', error.response?.data || error.message); res.status(500).json({ success: false, message: 'Failed to authorize. Please try again.' }); } }); // Admin routes app.get('/admin', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'admin.html')); }); app.post('/admin/login', (req, res) => { const { password } = req.body || {}; if (password === ADMIN_PASSWORD) { req.session.admin = true; return res.json({ success: true }); } res.status(401).json({ success: false, error: 'Invalid password' }); }); app.post('/admin/logout', (req, res) => { req.session.admin = false; res.json({ success: true }); }); app.get('/admin/check', (req, res) => { if (req.session && req.session.admin) { return res.json({ authenticated: true }); } res.json({ authenticated: false }); }); app.get('/admin/codes', requireAdmin, (req, res) => { const codes = db.loadCodes(); const now = new Date(); const filtered = codes.map((c) => ({ ...c, expired: c.expiresAt ? new Date(c.expiresAt) < now : false })); res.json({ codes: filtered }); }); app.post('/admin/codes', requireAdmin, (req, res) => { const { expiresInMinutes } = req.body || {}; const entry = db.addCode(expiresInMinutes != null ? Number(expiresInMinutes) : null); res.json({ success: true, code: entry.code, entry }); }); app.delete('/admin/codes/:code', requireAdmin, (req, res) => { const deleted = db.deleteCode(req.params.code); if (deleted) { return res.json({ success: true }); } res.status(404).json({ success: false, error: 'Code not found' }); }); // Public packages (active only) app.get('/api/packages', (req, res) => { const packages = db.loadPackages() .filter((p) => p.active) .sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)) .map((p) => ({ id: p.id, name: p.name, durationMinutes: p.durationMinutes, price: p.price, currency: p.currency })); res.json({ packages }); }); // Admin packages CRUD app.get('/admin/packages', requireAdmin, (req, res) => { const packages = db.loadPackages().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); res.json({ packages }); }); app.post('/admin/packages', requireAdmin, (req, res) => { const { name, durationMinutes, price, currency, active } = req.body || {}; if (!name || !durationMinutes || price == null) { return res.status(400).json({ success: false, error: 'Missing required fields: name, durationMinutes, price' }); } const entry = db.addPackage({ name, durationMinutes, price, currency: currency || 'EUR', active }); res.json({ success: true, package: entry }); }); app.put('/admin/packages/:id', requireAdmin, (req, res) => { const pkg = db.updatePackage(req.params.id, req.body || {}); if (!pkg) return res.status(404).json({ success: false, error: 'Package not found' }); res.json({ success: true, package: pkg }); }); app.delete('/admin/packages/:id', requireAdmin, (req, res) => { const deleted = db.deletePackage(req.params.id); if (!deleted) return res.status(404).json({ success: false, error: 'Package not found' }); res.json({ success: true }); }); // PayPal endpoints app.post('/api/paypal/create-order', async (req, res) => { const guestMac = req.session?.guestMac; if (!guestMac) { return res.status(400).json({ success: false, error: 'Session expired. Please reconnect to WiFi.' }); } const { packageId } = req.body || {}; const pkg = db.findPackageById(packageId); if (!pkg || !pkg.active) { return res.status(400).json({ success: false, error: 'Invalid package' }); } if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) { return res.status(500).json({ success: false, error: 'PayPal not configured' }); } try { const token = await getPayPalAccessToken(); const amount = String(Number(pkg.price).toFixed(2)); const resPayPal = await axios.post( `${PAYPAL_BASE_URL}/v2/checkout/orders`, { intent: 'CAPTURE', purchase_units: [{ amount: { currency_code: pkg.currency, value: amount }, description: pkg.name }] }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); const orderId = resPayPal.data.id; db.addPendingOrder(orderId, { guestMac: req.session.guestMac, packageId: pkg.id }); res.json({ success: true, orderId, clientId: PAYPAL_CLIENT_ID }); } catch (err) { console.error('PayPal create-order error:', err.response?.data || err.message); res.status(500).json({ success: false, error: 'Failed to create order' }); } }); app.post('/api/paypal/capture-order', async (req, res) => { const guestMac = req.session?.guestMac; const redirectUrl = req.session?.redirectUrl || 'https://google.com'; if (!guestMac) { return res.status(400).json({ success: false, error: 'Session expired. Please reconnect to WiFi.' }); } const { orderId } = req.body || {}; if (!orderId) { return res.status(400).json({ success: false, error: 'Missing orderId' }); } const pending = db.getAndRemovePendingOrder(orderId); if (!pending || pending.guestMac !== guestMac) { return res.status(400).json({ success: false, error: 'Invalid or expired order' }); } const pkg = db.findPackageById(pending.packageId); if (!pkg) { return res.status(400).json({ success: false, error: 'Package no longer available' }); } try { const token = await getPayPalAccessToken(); const resPayPal = await axios.post( `${PAYPAL_BASE_URL}/v2/checkout/orders/${orderId}/capture`, {}, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); if (resPayPal.data.status !== 'COMPLETED') { return res.status(400).json({ success: false, error: 'Payment not completed' }); } db.addCode(pkg.durationMinutes); // record for audit const amount = `${Number(pkg.price).toFixed(2)} ${pkg.currency}`; const durationText = pkg.durationMinutes >= 1440 ? `${pkg.durationMinutes / 1440} Tage` : pkg.durationMinutes >= 60 ? `${pkg.durationMinutes / 60} Stunden` : `${pkg.durationMinutes} Min`; sendTelegramNotification( `🛒 Neuer Zugangskauf\n\n` + `Paket: ${pkg.name}\n` + `Preis: ${amount}\n` + `Dauer: ${durationText}` ).catch(() => {}); await unifiApi.post( `${UNIFI_CONTROLLER}/proxy/network/api/s/${UNIFI_SITE}/cmd/stamgr`, { cmd: 'authorize-guest', mac: guestMac.toLowerCase(), minutes: pkg.durationMinutes } ); res.json({ success: true, message: 'Connected! Redirecting...', redirectUrl }); } catch (err) { console.error('PayPal capture-order error:', err.response?.data || err.message); res.status(500).json({ success: false, error: 'Failed to capture payment' }); } }); // PayPal client ID for frontend SDK app.get('/api/paypal/config', (req, res) => { res.json({ clientId: PAYPAL_CLIENT_ID || '' }); }); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); app.listen(PORT, '0.0.0.0', () => { console.log(`UniFi Guest Portal running on port ${PORT}`); console.log(`Make sure to configure UniFi to redirect to this portal`); });