Spaces:
Running
Running
| 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")); |