| |
| |
| |
|
|
| import express from 'express'; |
| import { createServer } from 'http'; |
| import cors from 'cors'; |
| import helmet from 'helmet'; |
| import morgan from 'morgan'; |
| import { Server } from 'socket.io'; |
| import dotenv from 'dotenv'; |
|
|
| dotenv.config(); |
|
|
| const app = express(); |
| const httpServer = createServer(app); |
|
|
| const PORT = process.env.PORT || 4000; |
| const CLIENT_URL = process.env.CLIENT_URL || 'http://localhost:3000'; |
|
|
| |
| app.use(helmet({ contentSecurityPolicy: false })); |
| app.use(cors({ origin: [CLIENT_URL, 'http://localhost:3000'], credentials: true })); |
| app.use(morgan('dev')); |
| app.use(express.json({ limit: '5mb' })); |
|
|
| |
| const io = new Server(httpServer, { |
| cors: { |
| origin: [CLIENT_URL, 'http://localhost:3000'], |
| methods: ['GET', 'POST'], |
| credentials: true, |
| }, |
| transports: ['websocket', 'polling'], |
| pingTimeout: 20000, |
| pingInterval: 10000, |
| maxHttpBufferSize: 1e6, |
| }); |
|
|
| |
| io.use((socket, next) => { |
| const token = socket.handshake.auth?.token; |
| if (!token) return next(new Error('Authentication required')); |
| |
| |
| (socket as any).userId = 'user-' + Math.random().toString(36).substr(2, 9); |
| (socket as any).userName = 'User'; |
| (socket as any).userColor = '#4ECDC4'; |
| (socket as any).currentRoom = null; |
| next(); |
| }); |
|
|
| |
| io.on('connection', (socket) => { |
| console.log(`Client connected: ${socket.id}`); |
|
|
| |
| socket.on('room:join', ({ roomId }) => { |
| socket.join(roomId); |
| (socket as any).currentRoom = roomId; |
| |
| socket.emit('room:joined', { |
| room: { id: roomId, name: 'Demo Room', language: 'javascript' }, |
| members: [], |
| files: [{ id: 'file-1', roomId, name: 'main.js', path: '/main.js', content: '// Start coding!\\n', language: 'javascript', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }], |
| }); |
| |
| socket.to(roomId).emit('room:member-joined', { |
| userId: (socket as any).userId, |
| userName: (socket as any).userName, |
| }); |
| }); |
|
|
| socket.on('room:leave', () => { |
| const roomId = (socket as any).currentRoom; |
| if (roomId) { |
| socket.leave(roomId); |
| socket.to(roomId).emit('room:member-left', { userId: (socket as any).userId }); |
| (socket as any).currentRoom = null; |
| } |
| }); |
|
|
| |
| socket.on('doc:update', (data) => { |
| const roomId = (socket as any).currentRoom; |
| if (roomId) { |
| socket.to(roomId).emit('doc:sync', data); |
| } |
| }); |
|
|
| socket.on('doc:request-state', (data) => { |
| socket.emit('doc:state', { state: [], fileId: data.fileId }); |
| }); |
|
|
| |
| socket.on('cursor:move', (data) => { |
| const roomId = (socket as any).currentRoom; |
| if (roomId) { |
| socket.to(roomId).emit('cursor:update', [{ |
| userId: (socket as any).userId, |
| userName: (socket as any).userName, |
| userColor: (socket as any).userColor, |
| ...data, |
| }]); |
| } |
| }); |
|
|
| |
| socket.on('chat:send', (data) => { |
| const roomId = (socket as any).currentRoom; |
| if (roomId) { |
| const message = { |
| id: Date.now().toString(), |
| content: data.content, |
| userId: (socket as any).userId, |
| userName: (socket as any).userName, |
| userAvatar: null, |
| roomId, |
| type: data.type || 'text', |
| createdAt: new Date().toISOString(), |
| }; |
| io.to(roomId).emit('chat:message', message); |
| } |
| }); |
|
|
| socket.on('chat:typing', (data) => { |
| const roomId = (socket as any).currentRoom; |
| if (roomId) { |
| socket.to(roomId).emit('chat:typing', { |
| userId: (socket as any).userId, |
| userName: (socket as any).userName, |
| isTyping: data.isTyping, |
| }); |
| } |
| }); |
|
|
| socket.on('chat:history', () => { |
| socket.emit('chat:history', []); |
| }); |
|
|
| |
| socket.on('exec:run', async (data) => { |
| const roomId = (socket as any).currentRoom; |
| socket.emit('exec:output', { id: 'exec-1', chunk: `[Running ${data.language}...]\\n`, stream: 'stdout' }); |
| |
| |
| setTimeout(() => { |
| socket.emit('exec:output', { id: 'exec-1', chunk: 'Hello, World!\\n', stream: 'stdout' }); |
| if (roomId) { |
| io.to(roomId).emit('exec:complete', { |
| id: 'exec-1', |
| stdout: 'Hello, World!\\n', |
| stderr: '', |
| exitCode: 0, |
| duration: 42, |
| memoryUsed: 1024, |
| timedOut: false, |
| }); |
| } |
| }, 500); |
| }); |
|
|
| |
| socket.on('webrtc:join', () => { |
| const roomId = (socket as any).currentRoom; |
| if (roomId) { |
| socket.to(roomId).emit('webrtc:peer-joined', { |
| peerId: socket.id, |
| peerName: (socket as any).userName, |
| }); |
| } |
| }); |
|
|
| socket.on('webrtc:offer', (data) => { |
| const target = io.sockets.sockets.get(data.to); |
| if (target) { |
| target.emit('webrtc:offer', { ...data, from: socket.id }); |
| } |
| }); |
|
|
| socket.on('webrtc:answer', (data) => { |
| const target = io.sockets.sockets.get(data.to); |
| if (target) { |
| target.emit('webrtc:answer', { ...data, from: socket.id }); |
| } |
| }); |
|
|
| socket.on('webrtc:ice-candidate', (data) => { |
| const target = io.sockets.sockets.get(data.to); |
| if (target) { |
| target.emit('webrtc:ice-candidate', { ...data, from: socket.id }); |
| } |
| }); |
|
|
| |
| socket.on('disconnect', () => { |
| const roomId = (socket as any).currentRoom; |
| if (roomId) { |
| socket.to(roomId).emit('room:member-left', { userId: (socket as any).userId }); |
| socket.to(roomId).emit('webrtc:peer-left', { peerId: socket.id }); |
| } |
| console.log(`Client disconnected: ${socket.id}`); |
| }); |
| }); |
|
|
| |
|
|
| app.get('/api/health', (_req, res) => { |
| res.json({ status: 'healthy', timestamp: new Date().toISOString(), version: '1.0.0' }); |
| }); |
|
|
| |
| app.post('/api/auth/register', (req, res) => { |
| const { email, name } = req.body; |
| const user = { id: 'user-' + Math.random().toString(36).substr(2, 9), email, name, avatar: null, provider: 'local', createdAt: new Date().toISOString() }; |
| const tokens = { accessToken: 'demo-token-' + user.id, refreshToken: 'refresh-' + user.id }; |
| res.status(201).json({ success: true, data: { user, tokens } }); |
| }); |
|
|
| app.post('/api/auth/login', (req, res) => { |
| const { email } = req.body; |
| const user = { id: 'user-' + Math.random().toString(36).substr(2, 9), email, name: email.split('@')[0], avatar: null, provider: 'local', createdAt: new Date().toISOString() }; |
| const tokens = { accessToken: 'demo-token-' + user.id, refreshToken: 'refresh-' + user.id }; |
| res.json({ success: true, data: { user, tokens } }); |
| }); |
|
|
| app.get('/api/rooms', (_req, res) => { |
| res.json({ success: true, data: [] }); |
| }); |
|
|
| app.post('/api/rooms', (req, res) => { |
| const { name, language, isPublic } = req.body; |
| const room = { |
| id: 'room-' + Math.random().toString(36).substr(2, 9), |
| name, language, isPublic, |
| ownerId: 'user-1', |
| inviteCode: Math.random().toString(36).substr(2, 8), |
| maxMembers: 10, |
| createdAt: new Date().toISOString(), |
| updatedAt: new Date().toISOString(), |
| }; |
| res.status(201).json({ success: true, data: room }); |
| }); |
|
|
| app.post('/api/ai/action', (req, res) => { |
| res.json({ success: true, data: { content: 'AI service requires OPENROUTER_API_KEY configuration.', suggestions: [] } }); |
| }); |
|
|
| app.get('/api/snippets', (_req, res) => { |
| res.json({ success: true, data: [] }); |
| }); |
|
|
| |
| app.use((_req, res) => { |
| res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: 'Endpoint not found' } }); |
| }); |
|
|
| |
| httpServer.listen(PORT, () => { |
| console.log(` |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| β CodeSync Server Running β |
| β βββββββββββββββββββββββββββββββββββββββββββββββββββ£ |
| β HTTP: http://localhost:${PORT} β |
| β WebSocket: ws://localhost:${PORT} β |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| `); |
| }); |
|
|
| export { app, httpServer, io }; |
|
|