epyks-server/server.js

798 lines
22 KiB
JavaScript

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');
});