Add Express server with WebSocket, CORS, and room routes
Set up the HTTP server with Express, attach Socket.IO for real-time messaging, add CORS middleware, async error handling utility, and REST routes for room join/leave/messages/participants. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
require('./server')
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
const { randomUUID } = require('crypto')
|
||||||
|
const { Server } = require('socket.io')
|
||||||
|
|
||||||
|
function createRealtimeServer(server, { clientOrigin, database, onError }) {
|
||||||
|
const io = new Server(server, {
|
||||||
|
cors: {
|
||||||
|
origin: clientOrigin,
|
||||||
|
methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
io.use(async (socket, next) => {
|
||||||
|
try {
|
||||||
|
const auth = socket.handshake.auth || {}
|
||||||
|
const query = socket.handshake.query || {}
|
||||||
|
const room = readParam(auth.room, query.room)
|
||||||
|
const userId = readParam(auth.userId, query.userId)
|
||||||
|
|
||||||
|
if (!room || !userId) {
|
||||||
|
return next(new Error('Missing room or userId'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await database.getUser(userId)
|
||||||
|
if (!user || user.room !== room) {
|
||||||
|
return next(new Error('User not authorized for this room'))
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.data.room = room
|
||||||
|
socket.data.userId = userId
|
||||||
|
next()
|
||||||
|
} catch (error) {
|
||||||
|
next(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
registerConnection(io, socket, database, onError).catch((error) => {
|
||||||
|
onError('Socket connection setup failed', error)
|
||||||
|
socket.disconnect(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return io
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerConnection(io, socket, database, onError) {
|
||||||
|
const { room, userId } = socket.data
|
||||||
|
socket.join(room)
|
||||||
|
|
||||||
|
const user = await database.setUserOnline(userId, room)
|
||||||
|
if (!user) {
|
||||||
|
socket.disconnect(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit('connection:ready', { userId, room })
|
||||||
|
socket.emit('history:init', await database.getMessages(room))
|
||||||
|
await broadcastParticipants(io, database, room)
|
||||||
|
|
||||||
|
socket.on('message:send', async (payload) => {
|
||||||
|
try {
|
||||||
|
const text =
|
||||||
|
typeof payload?.text === 'string' ? payload.text : payload?.payload?.text
|
||||||
|
if (!text || !text.trim()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeUser = await database.getUser(userId)
|
||||||
|
if (!activeUser || activeUser.room !== room) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await database.addMessage({
|
||||||
|
id: randomUUID(),
|
||||||
|
room,
|
||||||
|
userId,
|
||||||
|
username: activeUser.username,
|
||||||
|
text: text.trim(),
|
||||||
|
})
|
||||||
|
io.to(room).emit('message:new', message)
|
||||||
|
} catch (error) {
|
||||||
|
onError('Failed to handle socket message', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
database
|
||||||
|
.setUserOffline(userId)
|
||||||
|
.then(() => broadcastParticipants(io, database, room))
|
||||||
|
.catch((error) => onError('Failed to update presence', error))
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('error', (error) => {
|
||||||
|
onError('Socket error', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function broadcastParticipants(io, database, room) {
|
||||||
|
const participants = await database.getParticipants(room)
|
||||||
|
io.to(room).emit('participants:update', participants)
|
||||||
|
}
|
||||||
|
|
||||||
|
function readParam(primary, fallback) {
|
||||||
|
if (typeof primary === 'string' && primary.trim()) return primary
|
||||||
|
if (typeof fallback === 'string' && fallback.trim()) return fallback
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createRealtimeServer
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
function createCorsMiddleware(clientOrigin) {
|
||||||
|
return (req, res, next) => {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', clientOrigin)
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PATCH,DELETE,OPTIONS')
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return res.sendStatus(204)
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createCorsMiddleware
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const { randomUUID } = require('crypto')
|
||||||
|
const asyncHandler = require('../utils/asyncHandler')
|
||||||
|
|
||||||
|
function createRoomsRouter({ database, io }) {
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:room/join',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const { room } = req.params
|
||||||
|
const { username, userId } = req.body
|
||||||
|
|
||||||
|
if (!username || !username.trim()) {
|
||||||
|
return res.status(400).json({ error: 'Username is required' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = userId || randomUUID()
|
||||||
|
const user = await database.upsertUser({
|
||||||
|
id,
|
||||||
|
username: username.trim(),
|
||||||
|
room,
|
||||||
|
})
|
||||||
|
|
||||||
|
await broadcastParticipants(database, io, room)
|
||||||
|
res.json({ user })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
'/:room/users/:userId',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const { room, userId } = req.params
|
||||||
|
const { username } = req.body
|
||||||
|
|
||||||
|
if (!username || !username.trim()) {
|
||||||
|
return res.status(400).json({ error: 'Username is required' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await database.getUser(userId)
|
||||||
|
if (!user || user.room !== room) {
|
||||||
|
return res.status(404).json({ error: 'User not found in this room' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await database.updateUsername(userId, username.trim())
|
||||||
|
await broadcastParticipants(database, io, room)
|
||||||
|
res.json({ user: updated })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:room/leave',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const { room } = req.params
|
||||||
|
const { userId } = req.body
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(400).json({ error: 'userId is required' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await database.getUser(userId)
|
||||||
|
if (!user || user.room !== room) {
|
||||||
|
return res.status(404).json({ error: 'User not found in this room' })
|
||||||
|
}
|
||||||
|
|
||||||
|
await database.setUserOffline(userId)
|
||||||
|
await broadcastParticipants(database, io, room)
|
||||||
|
res.json({ success: true })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:room/messages',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const { room } = req.params
|
||||||
|
const messages = await database.getMessages(room)
|
||||||
|
res.json({ messages })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:room/participants',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const { room } = req.params
|
||||||
|
const participants = await database.getParticipants(room)
|
||||||
|
res.json({ participants })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:room/messages',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const { room } = req.params
|
||||||
|
const { userId, text } = req.body
|
||||||
|
|
||||||
|
if (!userId || !text || !text.trim()) {
|
||||||
|
return res.status(400).json({ error: 'userId and text are required' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await database.getUser(userId)
|
||||||
|
if (!user || user.room !== room) {
|
||||||
|
return res.status(404).json({ error: 'Unknown user or room mismatch' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await database.addMessage({
|
||||||
|
id: randomUUID(),
|
||||||
|
room,
|
||||||
|
userId,
|
||||||
|
username: user.username,
|
||||||
|
text: text.trim(),
|
||||||
|
})
|
||||||
|
|
||||||
|
io.to(room).emit('message:new', message)
|
||||||
|
res.status(201).json({ message })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
async function broadcastParticipants(database, io, room) {
|
||||||
|
const participants = await database.getParticipants(room)
|
||||||
|
io.to(room).emit('participants:update', participants)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createRoomsRouter
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const http = require('http')
|
||||||
|
const express = require('express')
|
||||||
|
const config = require('./config')
|
||||||
|
const createDataStore = require('./lib/datastore')
|
||||||
|
const createRealtimeServer = require('./lib/simpleWebSocket')
|
||||||
|
const createCorsMiddleware = require('./middleware/cors')
|
||||||
|
const createRoomsRouter = require('./routes/rooms')
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
const server = http.createServer(app)
|
||||||
|
|
||||||
|
app.use(express.json())
|
||||||
|
app.use(createCorsMiddleware(config.clientOrigin))
|
||||||
|
|
||||||
|
app.get('/health', (_req, res) => {
|
||||||
|
res.json({ status: 'ok', time: new Date().toISOString() })
|
||||||
|
})
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
try {
|
||||||
|
const database = await createDataStore()
|
||||||
|
const io = createRealtimeServer(server, {
|
||||||
|
clientOrigin: config.clientOrigin,
|
||||||
|
database,
|
||||||
|
onError: (message, error) => console.error(message, error),
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('/rooms', createRoomsRouter({ database, io }))
|
||||||
|
|
||||||
|
app.use((error, _req, res, _next) => {
|
||||||
|
console.error('Request failed', error)
|
||||||
|
res.status(500).json({ error: 'Internal server error' })
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen(config.port, () => {
|
||||||
|
console.log(`Chat backend running on http://localhost:${config.port}`)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Unable to start server:', error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap()
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
function asyncHandler(handler) {
|
||||||
|
return (req, res, next) => {
|
||||||
|
Promise.resolve(handler(req, res, next)).catch(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = asyncHandler
|
||||||
Reference in New Issue
Block a user