import { WebSocketGateway, WebSocketServer, SubscribeMessage, MessageBody, ConnectedSocket, OnGatewayConnection, OnGatewayDisconnect, } from '@nestjs/websockets'; import { UseGuards } from '@nestjs/common'; import { Server, Socket } from 'socket.io'; import { JwtService } from '@nestjs/jwt'; import { WsJwtGuard } from '../auth/guards'; import { ClassroomService } from './classroom.service'; @WebSocketGateway({ namespace: '/api/classroom', cors: { origin: [ 'http://localhost:5173', 'http://127.0.0.1:5173', 'http://localhost:5500', 'http://127.0.0.1:5500', 'http://localhost:3000' ], credentials: true, }, }) export class ClassroomGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; constructor( private readonly jwtService: JwtService, private readonly classroomService: ClassroomService, ) {} handleConnection(client: Socket) { try { const token = this.extractTokenFromHandshake(client); if (!token) { console.log(`Classroom: Client ${client.id} rejected: No token`); client.disconnect(); return; } const payload = this.jwtService.verify(token, { secret: process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-this-in-production', }); client.data.user = payload; // Send current state to newly connected user client.emit('state-update', { raisedHands: this.classroomService.getRaisedHands(), activeStudent: this.classroomService.getActiveStudent(), }); } catch (error) { console.log(`Classroom: Client ${client.id} rejected: Invalid token`); client.disconnect(); } } handleDisconnect(client: Socket) { // Optional: automatically lower hand on disconnect? // For now, let's keep it persistent for 10 mins as per requirements. } private extractTokenFromHandshake(client: Socket): string | null { const cookies = client.handshake.headers.cookie; if (cookies) { const cookieArray = cookies.split(';'); for (const cookie of cookieArray) { const [name, value] = cookie.trim().split('='); if (name === 'access_token') { return value; } } } return client.handshake.auth?.token || (client.handshake.query?.token as string) || null; } private broadcastState() { this.server.emit('state-update', { raisedHands: this.classroomService.getRaisedHands(), activeStudent: this.classroomService.getActiveStudent(), }); } @UseGuards(WsJwtGuard) @SubscribeMessage('raise-hand') handleRaiseHand( @MessageBody() data: { type: 'normal' | 'question' }, @ConnectedSocket() client: Socket, ) { const user = client.data.user; this.classroomService.raiseHand(user.sub, user.username, data.type, (userId) => { // Callback for timeout this.classroomService.lowerHand(userId); this.broadcastState(); }); this.broadcastState(); } @UseGuards(WsJwtGuard) @SubscribeMessage('lower-hand') handleLowerHand(@ConnectedSocket() client: Socket) { const user = client.data.user; this.classroomService.lowerHand(user.sub); this.broadcastState(); } @UseGuards(WsJwtGuard) @SubscribeMessage('lower-all-hands') handleLowerAllHands(@ConnectedSocket() client: Socket) { const user = client.data.user; if (user.role !== 'Lehrer') { client.emit('error', { message: 'Nur Lehrer können alle Hände senken.' }); return; } this.classroomService.lowerAllHands(); this.broadcastState(); } @UseGuards(WsJwtGuard) @SubscribeMessage('pick-student') handlePickStudent( @MessageBody() data: { userId: number }, @ConnectedSocket() client: Socket, ) { const user = client.data.user; if (user.role !== 'Lehrer') { client.emit('error', { message: 'Nur Lehrer können Schüler aufrufen.' }); return; } const hands = this.classroomService.getRaisedHands(); const target = hands.find(h => h.userId === data.userId); if (target) { this.classroomService.setActiveStudent(target.userId, target.username); this.broadcastState(); } } @UseGuards(WsJwtGuard) @SubscribeMessage('pick-random') handlePickRandom(@ConnectedSocket() client: Socket) { const user = client.data.user; if (user.role !== 'Lehrer') { client.emit('error', { message: 'Nur Lehrer können Zufallsauswahl nutzen.' }); return; } this.classroomService.pickRandomStudent(); this.broadcastState(); } @UseGuards(WsJwtGuard) @SubscribeMessage('reset-active') handleResetActive(@ConnectedSocket() client: Socket) { const user = client.data.user; const activeStudent = this.classroomService.getActiveStudent(); // Allow if teacher OR if the requesting user is the active student const isActiveStudent = activeStudent && activeStudent.userId === user.sub; if (user.role !== 'Lehrer' && !isActiveStudent) { client.emit('error', { message: 'Nur Lehrer oder der aktive Schüler können die Auswahl zurücksetzen.' }); return; } this.classroomService.clearActiveStudent(); this.broadcastState(); } }