const express = require('express'); const cloudinary = require('cloudinary').v2; const multer = require('multer'); const cookieSession = require('cookie-session'); const bcrypt = require('bcryptjs'); const axios = require('axios'); const streamifier = require('streamifier'); const app = express(); const upload = multer({ storage: multer.memoryStorage() }); // Essential for Hugging Face Spaces proxy app.set('trust proxy', 1); cloudinary.config({ cloud_name: process.env.CLOUDINARY_CLOUD_NAME, api_key: process.env.CLOUDINARY_API_KEY, api_secret: process.env.CLOUDINARY_API_SECRET }); const DB_FILE = 'megapin_master_db.json'; const MAX_TRENDING = 100; // SECRETS const ADMIN_USER = process.env.ADMIN_USER; const ADMIN_PASS = process.env.ADMIN_PASS; const DEFAULT_AVATAR = "https://cdn-icons-png.flaticon.com/512/149/149071.png"; let localDB = null; app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.set('view engine', 'ejs'); // --- SESSION CONFIG --- app.use(cookieSession({ name: 'mp_session', keys: [process.env.SESSION_SECRET || 'secret_key_change_me'], maxAge: 24 * 60 * 60 * 1000, secure: true, sameSite: 'none', httpOnly: true })); // --- DB ENGINE --- async function initDB() { if (localDB) return localDB; try { const url = cloudinary.url(DB_FILE, { resource_type: 'raw' }) + `?t=${Date.now()}`; const res = await axios.get(url); localDB = res.data; if (!localDB.users) localDB.users = {}; if (!localDB.pins) localDB.pins = []; } catch (e) { localDB = { users: {}, pins: [] }; } return localDB; } async function saveDB() { const buffer = Buffer.from(JSON.stringify(localDB)); return new Promise((resolve, reject) => { const stream = cloudinary.uploader.upload_stream( { resource_type: 'raw', public_id: DB_FILE, overwrite: true, invalidate: true }, (err, res) => { if (err) { console.error("Backup failed:", err); reject(err); } else resolve(res); } ); streamifier.createReadStream(buffer).pipe(stream); }); } const uploadImage = (buffer) => { return new Promise((resolve, reject) => { const stream = cloudinary.uploader.upload_stream({ folder: "megapin_assets" }, (err, res) => { if (res) resolve(res); else reject(err); }); streamifier.createReadStream(buffer).pipe(stream); }); }; // --- MIDDLEWARE --- function addNotification(toUser, fromUser, type, pinId = null, preview = "") { if (toUser === fromUser) return; if (!localDB.users[toUser]) return; if (!localDB.users[toUser].notifications) localDB.users[toUser].notifications = []; localDB.users[toUser].notifications.unshift({ from: fromUser, fromAvatar: localDB.users[fromUser].avatar || DEFAULT_AVATAR, type: type, pinId: pinId, preview: preview, timestamp: Date.now(), read: false }); if (localDB.users[toUser].notifications.length > 50) localDB.users[toUser].notifications.pop(); } async function checkBan(req, res, next) { if (!req.session.user) return next(); await initDB(); const user = localDB.users[req.session.user]; if (!user) { req.session = null; return res.redirect('/'); } if (user.banned) { req.session = null; return res.send("Account Suspended."); } next(); } function checkAdmin(req, res, next) { if (req.session.user === ADMIN_USER) return next(); res.status(403).json({ error: "Admin only" }); } function formatPins(pins, username) { return pins.map(pin => { const authorData = localDB.users[pin.author] || { avatar: DEFAULT_AVATAR, displayName: pin.author + " (Deleted)", badge: '' }; return { ...pin, authorDisplayName: authorData.displayName || pin.author, authorAvatar: authorData.avatar || DEFAULT_AVATAR, badge: authorData.badge || '', likeCount: pin.likes ? pin.likes.length : 0, hasLiked: pin.likes && username ? pin.likes.includes(username) : false, comments: pin.comments || [] }; }); } // --- ROUTES --- app.get('/', async (req, res) => { await initDB(); const username = req.session.user; let currentUser = null; let notifications = []; if (username && localDB.users[username]) { currentUser = { ...localDB.users[username], username }; if (!currentUser.notifications) currentUser.notifications = []; notifications = currentUser.notifications; } let viewPins = localDB.pins; if (req.query.q) { const q = req.query.q.toLowerCase(); viewPins = localDB.pins.filter(p => p.author.toLowerCase().includes(q) || (p.caption && p.caption.toLowerCase().includes(q)) ); } const formattedPins = formatPins(viewPins, username); res.render('index', { pins: formattedPins, user: currentUser, notifications, isAdmin: (username === ADMIN_USER), searchQuery: req.query.q || '', pageType: 'home', profileUser: null }); }); app.get('/u/:username', async (req, res) => { await initDB(); const username = req.session.user; const targetUsername = req.params.username; if (!localDB.users[targetUsername]) return res.redirect('/'); const targetUser = localDB.users[targetUsername]; const userPins = localDB.pins.filter(p => p.author === targetUsername); const formattedPins = formatPins(userPins, username); const profileUser = { username: targetUsername, ...targetUser, postCount: userPins.length, followerCount: targetUser.followers ? targetUser.followers.length : 0, followingCount: targetUser.following ? targetUser.following.length : 0, isFollowing: targetUser.followers && targetUser.followers.includes(username), badge: targetUser.badge || '' }; let currentUser = null; if (username && localDB.users[username]) currentUser = { ...localDB.users[username], username }; res.render('index', { pins: formattedPins, user: currentUser, notifications: currentUser ? currentUser.notifications : [], isAdmin: (username === ADMIN_USER), pageType: 'profile', profileUser: profileUser }); }); // --- API --- app.get('/api/pin/:id', async (req, res) => { await initDB(); const pin = localDB.pins.find(p => p.id === parseInt(req.params.id)); if(!pin) return res.json({error: "Not found"}); const formatted = formatPins([pin], req.session.user)[0]; res.json(formatted); }); app.get('/api/search-users', async (req, res) => { await initDB(); const q = req.query.q ? req.query.q.toLowerCase() : ''; const users = Object.keys(localDB.users).filter(u => u.toLowerCase().includes(q)); const results = users.map(u => ({ username: u, avatar: localDB.users[u].avatar || DEFAULT_AVATAR, badge: localDB.users[u].badge || '' })); res.json(results); }); app.post('/api/like', checkBan, async (req, res) => { if (!req.session.user) return res.status(401).json({error: "Login required"}); await initDB(); const { pinId } = req.body; const pin = localDB.pins.find(p => p.id === parseInt(pinId)); let liked = false; if (pin) { if (!pin.likes) pin.likes = []; const index = pin.likes.indexOf(req.session.user); if (index === -1) { pin.likes.push(req.session.user); liked = true; addNotification(pin.author, req.session.user, 'like', pin.id); } else { pin.likes.splice(index, 1); } await saveDB(); return res.json({ success: true, liked, count: pin.likes.length }); } res.json({ success: false }); }); app.post('/api/follow', checkBan, async (req, res) => { if (!req.session.user) return res.status(401).json({error: "Login required"}); await initDB(); const { targetUser } = req.body; const me = req.session.user; if (!localDB.users[targetUser] || targetUser === me) return res.json({ success: false }); const targetData = localDB.users[targetUser]; const myData = localDB.users[me]; if (!targetData.followers) targetData.followers = []; if (!myData.following) myData.following = []; let isFollowing = false; const idx = targetData.followers.indexOf(me); if (idx === -1) { targetData.followers.push(me); myData.following.push(targetUser); isFollowing = true; addNotification(targetUser, me, 'follow'); } else { targetData.followers.splice(idx, 1); const myIdx = myData.following.indexOf(targetUser); if (myIdx !== -1) myData.following.splice(myIdx, 1); } await saveDB(); res.json({ success: true, isFollowing, followersCount: targetData.followers.length }); }); app.post('/api/comment', checkBan, async (req, res) => { if (!req.session.user) return res.status(401).json({error: "Login required"}); await initDB(); const { pinId, text } = req.body; if(!text || !text.trim()) return res.json({success:false}); const pin = localDB.pins.find(p => p.id === parseInt(pinId)); if (pin) { if (!pin.comments) pin.comments = []; const comment = { id: Date.now(), user: req.session.user, avatar: localDB.users[req.session.user].avatar, text: text, timestamp: Date.now() }; pin.comments.push(comment); addNotification(pin.author, req.session.user, 'comment', pin.id, text); await saveDB(); return res.json({ success: true, comment }); } res.json({ success: false }); }); app.post('/api/clear-notifications', async (req, res) => { if (!req.session.user) return; await initDB(); if(localDB.users[req.session.user]) { localDB.users[req.session.user].notifications = []; await saveDB(); } res.json({success: true}); }); // --- ADMIN API --- app.post('/api/admin/badge', checkAdmin, async (req, res) => { await initDB(); const { targetUser, badge } = req.body; if(localDB.users[targetUser]) { localDB.users[targetUser].badge = badge; await saveDB(); res.json({success: true}); } else { res.json({success: false}); } }); app.post('/api/admin/ban', checkAdmin, async (req, res) => { await initDB(); const { targetUser, banned } = req.body; if(localDB.users[targetUser] && targetUser !== ADMIN_USER) { localDB.users[targetUser].banned = banned; await saveDB(); res.json({success: true}); } else { res.json({success: false}); } }); app.post('/api/admin/delete-user', checkAdmin, async (req, res) => { await initDB(); const { targetUser } = req.body; if(localDB.users[targetUser] && targetUser !== ADMIN_USER) { delete localDB.users[targetUser]; localDB.pins = localDB.pins.filter(p => p.author !== targetUser); await saveDB(); res.json({success: true}); } else { res.json({success: false}); } }); // --- FORMS --- app.post('/signup', async (req, res) => { const { username, password, email, gender } = req.body; // Added gender await initDB(); if(!username || !password) return res.send("Username and Password are required."); const safeUser = username.trim(); if (localDB.users[safeUser]) return res.send("Username taken."); localDB.users[safeUser] = { displayName: safeUser, hash: bcrypt.hashSync(password, 8), email: email || '', gender: gender || 'Not Specified', // Save gender avatar: DEFAULT_AVATAR, followers: [], following: [], notifications: [], banned: false }; await saveDB(); req.session.user = safeUser; res.redirect('/'); }); app.post('/login', async (req, res) => { const { username, password } = req.body; await initDB(); if(!username || !password) return res.send("Missing credentials."); if (username === ADMIN_USER && password === ADMIN_PASS) { req.session.user = username; return res.redirect('/'); } const user = localDB.users[username]; if (user && bcrypt.compareSync(password, user.hash)) { if(user.banned) return res.send("Banned."); req.session.user = username; res.redirect('/'); } else { res.send("Invalid credentials."); } }); app.post('/add', checkBan, upload.single('image'), async (req, res) => { if (!req.session.user || !req.file) return res.redirect('/'); await initDB(); try { const result = await uploadImage(req.file.buffer); const finalUrl = result.secure_url; localDB.pins.unshift({ id: Date.now(), url: finalUrl, caption: req.body.caption, author: req.session.user, likes: [], comments: [], timestamp: Date.now() }); if (localDB.pins.length > MAX_TRENDING) localDB.pins = localDB.pins.slice(0, MAX_TRENDING); await saveDB(); } catch(err) { console.error("Upload Error", err); } res.redirect('/'); }); app.post('/update-profile', upload.single('avatar'), checkBan, async (req, res) => { if (!req.session.user) return res.redirect('/'); await initDB(); const user = localDB.users[req.session.user]; if (req.body.bio) user.bio = req.body.bio; if (req.body.display_name) user.displayName = req.body.display_name; if (req.file) { try { const result = await uploadImage(req.file.buffer); user.avatar = result.secure_url; } catch(e) { console.error(e); } } await saveDB(); res.redirect('/u/' + req.session.user); }); app.post('/delete', checkBan, async (req, res) => { if (!req.session.user) return res.redirect('/'); await initDB(); const id = parseInt(req.body.id); const pin = localDB.pins.find(p => p.id === id); if (pin && (pin.author === req.session.user || req.session.user === ADMIN_USER)) { localDB.pins = localDB.pins.filter(p => p.id !== id); await saveDB(); } res.redirect('/'); }); app.get('/logout', (req, res) => { req.session = null; res.redirect('/'); }); initDB(); app.listen(7860, () => console.log("Megapin Final Aesthetic Active"));