const express = require('express'); const app = express(); const http = require('http').createServer(app); const io = require('socket.io')(http); const { Sequelize, DataTypes } = require('sequelize'); const fs = require('fs'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const { v4: uuidv4 } = require('uuid'); const multer = require('multer'); const path = require('path'); require('dotenv').config(); // Load configuration const config = JSON.parse(fs.readFileSync('config.json')); // Initialize Sequelize let sequelize; if (config.database.type === 'sqlite') { sequelize = new Sequelize({ dialect: 'sqlite', storage: config.database.storage, }); } else if (config.database.type === 'mysql') { sequelize = new Sequelize( config.database.database, config.database.username, config.database.password, { host: config.database.host, dialect: 'mysql', } ); } else if (config.database.type === 'postgres') { sequelize = new Sequelize( config.database.database, config.database.username, config.database.password, { host: config.database.host, dialect: 'postgres', } ); } // Define models const User = sequelize.define('User', { id: { type: DataTypes.STRING, primaryKey: true, }, email: { type: DataTypes.STRING, unique: true, allowNull: false, }, nickname: { type: DataTypes.STRING, allowNull: false, }, password: { type: DataTypes.STRING, allowNull: true, }, type: { type: DataTypes.STRING, allowNull: false, defaultValue: 'permanent', }, expiresAt: { type: DataTypes.DATE, allowNull: true, }, avatarUrl: { type: DataTypes.STRING, allowNull: true, }, }); const Contact = sequelize.define('Contact', { userId: { type: DataTypes.STRING, allowNull: false, }, contactId: { type: DataTypes.STRING, allowNull: false, }, contactEmail: { type: DataTypes.STRING, allowNull: false, }, contactNickname: { type: DataTypes.STRING, allowNull: false, }, }); const Room = sequelize.define('Room', { id: { type: DataTypes.STRING, primaryKey: true, }, name: { type: DataTypes.STRING, allowNull: false, }, creatorId: { type: DataTypes.STRING, allowNull: false, }, }); const Message = sequelize.define('Message', { senderId: { type: DataTypes.STRING, allowNull: false, }, receiverId: { type: DataTypes.STRING, allowNull: false, }, roomId: { type: DataTypes.STRING, allowNull: true, }, senderNickname: { type: DataTypes.STRING, allowNull: false, }, message: { type: DataTypes.TEXT, allowNull: true, }, timestamp: { type: DataTypes.DATE, allowNull: false, }, avatarUrl: { type: DataTypes.STRING, allowNull: true, }, fileUrl: { type: DataTypes.STRING, allowNull: true, }, fileName: { type: DataTypes.STRING, allowNull: true, }, reactions: { type: DataTypes.JSON, allowNull: true, defaultValue: {}, }, }); const Meeting = sequelize.define('Meeting', { id: { type: DataTypes.STRING, primaryKey: true, }, title: { type: DataTypes.STRING, allowNull: false, }, scheduledTime: { type: DataTypes.DATE, allowNull: false, }, duration: { type: DataTypes.INTEGER, // Duration in minutes allowNull: false, }, hostId: { type: DataTypes.STRING, allowNull: false, }, participants: { type: DataTypes.JSON, // List of user IDs in the meeting allowNull: true, defaultValue: [], }, waitingRoom: { type: DataTypes.JSON, // List of user IDs in the waiting room allowNull: true, defaultValue: [], }, breakoutRooms: { type: DataTypes.JSON, // Map of breakout room IDs to user IDs allowNull: true, defaultValue: {}, }, status: { type: DataTypes.STRING, allowNull: false, defaultValue: 'scheduled', // scheduled, active, ended }, }); // Sync database sequelize.sync({ alter: true }).then(() => { console.log('Database synced'); }); // Set up file upload const upload = multer({ dest: 'uploads/', fileFilter: (req, file, cb) => { console.log('Received file with MIME type:', file.mimetype); const allowedMimeTypes = [ 'image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp', 'application/pdf', 'text/plain', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', ]; if (allowedMimeTypes.includes(file.mimetype)) { cb(null, true); } else { const extname = path.extname(file.originalname).toLowerCase(); const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.pdf', '.txt', '.doc', '.docx']; if (allowedExtensions.includes(extname)) { console.log('MIME type not recognized, but file extension is valid:', extname); cb(null, true); } else { console.log('File rejected: Not an allowed file type'); cb(new Error('Only images, PDFs, text, and Word documents are allowed'), false); } } }, }); // Serve uploaded files statically app.use('/uploads', express.static('uploads')); // JWT secret from environment variable const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { console.error('JWT_SECRET is not defined in .env file'); process.exit(1); } console.log('Using JWT_SECRET:', JWT_SECRET); // Load BASE_URL from environment variable const BASE_URL = process.env.BASE_URL || 'http://localhost:3000'; console.log('Using BASE_URL:', BASE_URL); // JWT middleware const authenticateJWT = async (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { console.log('No token provided'); return res.status(401).json({ error: 'Unauthorized: No token provided' }); } try { console.log('Verifying token with JWT_SECRET:', JWT_SECRET); const decoded = jwt.verify(token, JWT_SECRET); console.log('JWT decoded successfully:', decoded); req.userId = decoded.userId; next(); } catch (e) { console.log('Invalid token:', e.message); res.status(401).json({ error: `Invalid token: ${e.message}` }); } }; // Middleware app.use(express.json()); // API endpoints app.post('/auth/login', async (req, res) => { const { username, password } = req.body; // Username is the email console.log('Login attempt:', { username }); const user = await User.findOne({ where: { email: username } }); if (!user || !user.password) { console.log('User not found or no password'); return res.status(401).json({ error: 'Invalid email or password' }); } const isValid = await bcrypt.compare(password, user.password); if (!isValid) { console.log('Invalid password'); return res.status(401).json({ error: 'Invalid email or password' }); } console.log('Generating token with userId:', user.id); console.log('Signing token with JWT_SECRET:', JWT_SECRET); const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' }); console.log('Generated JWT for login:', token); res.json({ token }); }); app.post('/auth/register', async (req, res) => { const { username, nickname, password } = req.body; // Username is the email console.log('Register attempt:', { username, nickname }); try { const id = uuidv4(); const hashedPassword = await bcrypt.hash(password, 10); const user = await User.create({ id, email: username, nickname, password: hashedPassword, type: 'permanent', }); console.log('Generating token with userId:', id); console.log('Signing token with JWT_SECRET:', JWT_SECRET); const token = jwt.sign({ userId: id }, JWT_SECRET, { expiresIn: '7d' }); console.log('Generated JWT for register:', token); res.status(201).json({ token }); } catch (e) { console.log('Registration error:', e.message); res.status(400).json({ error: e.message }); } }); app.post('/auth/burner', async (req, res) => { try { const id = uuidv4(); const email = `guest_${id.substring(0, 8)}@epyks.com`; const user = await User.create({ id, email, nickname: `Guest_${id.substring(0, 8)}`, type: 'burner', expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), }); console.log('Generating token with userId:', id); console.log('Signing token with JWT_SECRET:', JWT_SECRET); const token = jwt.sign({ userId: id }, JWT_SECRET, { expiresIn: '24h' }); console.log('Generated JWT for burner:', token); res.status(201).json({ token }); } catch (e) { console.log('Burner account error:', e.message); res.status(400).json({ error: e.message }); } }); app.get('/users/me', authenticateJWT, async (req, res) => { const user = await User.findByPk(req.userId); if (user) { res.json(user); } else { console.log('User not found for ID:', req.userId); res.status(404).json({ error: 'User not found' }); } }); app.get('/users/:id', authenticateJWT, async (req, res) => { const user = await User.findByPk(req.params.id); if (user) { res.json(user); } else { res.status(404).json({ error: 'User not found' }); } }); app.get('/users', authenticateJWT, async (req, res) => { const { email } = req.query; const user = await User.findOne({ where: { email } }); if (user) { res.json(user); } else { res.status(404).json({ error: 'User not found' }); } }); app.get('/contacts', authenticateJWT, async (req, res) => { const { userId } = req.query; const contacts = await Contact.findAll({ where: { userId } }); res.json(contacts); }); app.post('/contacts', authenticateJWT, async (req, res) => { try { const contact = await Contact.create(req.body); res.status(201).json(contact); } catch (e) { res.status(400).json({ error: e.message }); } }); app.get('/rooms', authenticateJWT, async (req, res) => { try { const rooms = await Room.findAll(); res.status(200).json(rooms); } catch (e) { console.log('Error fetching rooms:', e.message); res.status(500).json({ error: 'Server error fetching rooms' }); } }); app.post('/rooms', authenticateJWT, async (req, res) => { const { name } = req.body; if (!name) { return res.status(400).json({ error: 'Room name is required' }); } try { const id = uuidv4(); const room = await Room.create({ id, name, creatorId: req.userId, }); res.status(201).json(room); } catch (e) { console.log('Error creating room:', e.message); res.status(400).json({ error: 'Error creating room' }); } }); app.post('/messages', authenticateJWT, async (req, res) => { try { const message = await Message.create(req.body); res.status(201).json(message); } catch (e) { console.log('Error creating message:', e.message); res.status(400).json({ error: 'Error creating message' }); } }); app.get('/messages', authenticateJWT, async (req, res) => { const { roomId } = req.query; if (!roomId) { return res.status(400).json({ error: 'roomId is required' }); } try { const messages = await Message.findAll({ where: { roomId } }); res.status(200).json(messages); } catch (e) { console.log('Error fetching messages:', e.message); res.status(500).json({ error: 'Server error fetching messages' }); } }); app.post('/users/update', authenticateJWT, upload.single('avatar'), async (req, res) => { try { const user = await User.findByPk(req.userId); if (!user) { return res.status(404).json({ error: 'User not found' }); } if (req.file) { const avatarPath = `/uploads/${req.file.filename}`; const avatarUrl = `${BASE_URL}${avatarPath}`; console.log('Avatar uploaded successfully:', avatarUrl); await user.update({ avatarUrl }); res.json({ avatarUrl }); } else { console.log('No file uploaded'); res.status(400).json({ error: 'No file uploaded' }); } } catch (e) { console.log('Error in /users/update:', e.message); res.status(400).json({ error: e.message }); } }); app.post('/messages/upload', authenticateJWT, upload.single('file'), async (req, res) => { try { const { roomId, senderId, senderNickname, timestamp } = req.body; if (!roomId || !senderId || !senderNickname || !timestamp) { return res.status(400).json({ error: 'Missing required fields' }); } if (req.file) { const filePath = `/uploads/${req.file.filename}`; const fileUrl = `${BASE_URL}${filePath}`; const fileName = req.file.originalname; console.log('File uploaded successfully:', fileUrl); const message = await Message.create({ senderId, receiverId: senderId, roomId, senderNickname, timestamp, fileUrl, fileName, }); io.to(roomId).emit('chat-message', { id: message.id, senderId, senderNickname, timestamp, roomId, fileUrl, fileName, reactions: message.reactions || {}, }); res.json({ fileUrl, fileName }); } else { console.log('No file uploaded'); res.status(400).json({ error: 'No file uploaded' }); } } catch (e) { console.log('Error in /messages/upload:', e.message); res.status(400).json({ error: e.message }); } }); app.post('/messages/:id/react', authenticateJWT, async (req, res) => { try { const { id } = req.params; const { emoji } = req.body; const userId = req.userId; if (!emoji) { return res.status(400).json({ error: 'Emoji is required' }); } const message = await Message.findByPk(id); if (!message) { return res.status(404).json({ error: 'Message not found' }); } let reactions = message.reactions || {}; if (reactions[emoji]) { const userIndex = reactions[emoji].indexOf(userId); if (userIndex !== -1) { reactions[emoji].splice(userIndex, 1); if (reactions[emoji].length === 0) { delete reactions[emoji]; } } else { reactions[emoji].push(userId); } } else { reactions[emoji] = [userId]; } await message.update({ reactions }); io.to(message.roomId).emit('reaction-update', { messageId: id, reactions: message.reactions, }); res.status(200).json({ reactions: message.reactions }); } catch (e) { console.log('Error in /messages/:id/react:', e.message); res.status(400).json({ error: e.message }); } }); // Meeting Management Endpoints app.post('/meetings/schedule', authenticateJWT, async (req, res) => { try { const { title, scheduledTime, duration } = req.body; if (!title || !scheduledTime || !duration) { return res.status(400).json({ error: 'Title, scheduledTime, and duration are required' }); } const id = uuidv4(); const meeting = await Meeting.create({ id, title, scheduledTime, duration, hostId: req.userId, status: 'scheduled', }); res.status(201).json(meeting); } catch (e) { console.log('Error scheduling meeting:', e.message); res.status(400).json({ error: e.message }); } }); app.get('/meetings', authenticateJWT, async (req, res) => { try { const meetings = await Meeting.findAll({ where: { [Sequelize.Op.or]: [ { hostId: req.userId }, { participants: { [Sequelize.Op.contains]: [req.userId] } }, ], }, }); res.status(200).json(meetings); } catch (e) { console.log('Error fetching meetings:', e.message); res.status(500).json({ error: 'Server error fetching meetings' }); } }); app.post('/meetings/:id/join', authenticateJWT, async (req, res) => { try { const { id } = req.params; const userId = req.userId; const meeting = await Meeting.findByPk(id); if (!meeting) { return res.status(404).json({ error: 'Meeting not found' }); } if (meeting.hostId === userId) { // Host joins directly let participants = meeting.participants || []; if (!participants.includes(userId)) { participants.push(userId); await meeting.update({ participants, status: 'active' }); } io.to(id).emit('participant-admitted', { userId, meetingId: id }); res.status(200).json(meeting); } else { // Non-host joins the waiting room let waitingRoom = meeting.waitingRoom || []; if (!waitingRoom.includes(userId) && !meeting.participants.includes(userId)) { waitingRoom.push(userId); await meeting.update({ waitingRoom }); io.to(id).emit('waiting-room-update', { meetingId: id, waitingRoom }); } res.status(200).json({ status: 'waiting', meeting }); } } catch (e) { console.log('Error joining meeting:', e.message); res.status(400).json({ error: e.message }); } }); app.post('/meetings/:id/admit', authenticateJWT, async (req, res) => { try { const { id } = req.params; const { userId } = req.body; const hostId = req.userId; const meeting = await Meeting.findByPk(id); if (!meeting) { return res.status(404).json({ error: 'Meeting not found' }); } if (meeting.hostId !== hostId) { return res.status(403).json({ error: 'Only the host can admit participants' }); } let waitingRoom = meeting.waitingRoom || []; let participants = meeting.participants || []; const userIndex = waitingRoom.indexOf(userId); if (userIndex !== -1) { waitingRoom.splice(userIndex, 1); participants.push(userId); await meeting.update({ waitingRoom, participants }); io.to(id).emit('waiting-room-update', { meetingId: id, waitingRoom }); io.to(id).emit('participant-admitted', { userId, meetingId: id }); } res.status(200).json(meeting); } catch (e) { console.log('Error admitting participant:', e.message); res.status(400).json({ error: e.message }); } }); app.post('/meetings/:id/breakout', authenticateJWT, async (req, res) => { try { const { id } = req.params; const { rooms } = req.body; // { room1: [userId1, userId2], room2: [userId3] } const hostId = req.userId; const meeting = await Meeting.findByPk(id); if (!meeting) { return res.status(404).json({ error: 'Meeting not found' }); } if (meeting.hostId !== hostId) { return res.status(403).json({ error: 'Only the host can create breakout rooms' }); } await meeting.update({ breakoutRooms: rooms }); io.to(id).emit('breakout-rooms-update', { meetingId: id, breakoutRooms: rooms }); res.status(200).json(meeting); } catch (e) { console.log('Error creating breakout rooms:', e.message); res.status(400).json({ error: e.message }); } }); // Cleanup expired burner accounts setInterval(async () => { await User.destroy({ where: { type: 'burner', expiresAt: { [Sequelize.Op.lt]: new Date() }, }, }); }, 60 * 60 * 1000); // Socket.io with JWT io.use((socket, next) => { const token = socket.handshake.auth.token; if (!token) { return next(new Error('Authentication error')); } try { console.log('Verifying Socket.io token with JWT_SECRET:', JWT_SECRET); const decoded = jwt.verify(token, JWT_SECRET); socket.userId = decoded.userId; next(); } catch (e) { console.log('Socket.io auth error:', e.message); next(new Error('Authentication error')); } }); io.on('connection', (socket) => { console.log('User connected:', socket.userId); socket.on('join', (data) => { socket.join(data.userId); socket.broadcast.emit('user-joined', { userId: data.userId }); }); socket.on('join-room', (data) => { const { roomId } = data; socket.join(roomId); console.log(`User ${socket.userId} joined room ${roomId}`); }); socket.on('join-meeting', (data) => { const { meetingId } = data; socket.join(meetingId); console.log(`User ${socket.userId} joined meeting ${meetingId}`); }); socket.on('leave-room', (data) => { const { roomId } = data; socket.leave(roomId); console.log(`User ${socket.userId} left room ${roomId}`); }); socket.on('leave-meeting', (data) => { const { meetingId } = data; socket.leave(meetingId); console.log(`User ${socket.userId} left meeting ${meetingId}`); }); socket.on('offer', (data) => { socket.to(data.to).emit('offer', { sdp: data.sdp, type: data.type, from: data.from, }); }); socket.on('answer', (data) => { socket.to(data.to).emit('answer', { sdp: data.sdp, type: data.type }); }); socket.on('ice-candidate', (data) => { socket.to(data.to).emit('ice-candidate', { candidate: data.candidate }); }); socket.on('chat-message', async (data) => { const { roomId } = data; const message = await Message.create({ senderId: data.senderId, receiverId: data.senderId, roomId: data.roomId, senderNickname: data.senderNickname, message: data.message, timestamp: data.timestamp, avatarUrl: data.avatarUrl, fileUrl: data.fileUrl, fileName: data.fileName, }); io.to(roomId).emit('chat-message', { id: message.id, senderId: data.senderId, senderNickname: data.senderNickname, message: data.message, timestamp: data.timestamp, roomId: data.roomId, avatarUrl: data.avatarUrl, fileUrl: data.fileUrl, fileName: data.fileName, reactions: message.reactions || {}, }); }); socket.on('disconnect', () => { console.log('User disconnected:', socket.userId); }); }); http.listen(3000, () => { console.log('Server running on port 3000'); });