import { ConnectedSocket, MessageBody, OnGatewayConnection, SubscribeMessage, WebSocketGateway, WebSocketServer, } from '@nestjs/websockets'; import { JwtService } from '@nestjs/jwt'; import { Server, Socket } from 'socket.io'; import { AuthService } from '../auth/auth.service'; import { SupabaseService } from '../auth/supabase.service'; import { UsersService } from '../users/users.service'; import { GroupsService } from './groups.service'; @WebSocketGateway({ namespace: '/ws/course-groups', cors: { origin: true, credentials: true, }, }) export class GroupsGateway implements OnGatewayConnection { @WebSocketServer() server: Server; constructor( private jwtService: JwtService, private authService: AuthService, private supabaseService: SupabaseService, private usersService: UsersService, private groupsService: GroupsService ) {} async handleConnection(client: Socket): Promise { try { const user = await this.resolveUser(client); if (!user) { client.disconnect(); return; } client.data.user = user; } catch { client.disconnect(); } } @SubscribeMessage('groups:join') async joinGroup( @ConnectedSocket() client: Socket, @MessageBody() body: { groupId: string } ) { const user = client.data.user; if (!user || !body?.groupId) return { ok: false }; const canJoin = await this.groupsService.isMember(body.groupId, user.id); if (!canJoin) { await this.groupsService.joinByInvite(body.groupId, user.id); } await client.join(this.room(body.groupId)); const messages = await this.groupsService.getGroupMessages(body.groupId, user.id); return { ok: true, messages }; } @SubscribeMessage('groups:send') async sendMessage( @ConnectedSocket() client: Socket, @MessageBody() body: { groupId: string; content: string; lessonId?: string } ) { const user = client.data.user; if (!user || !body?.groupId || !body?.content?.trim()) return { ok: false }; const message = await this.groupsService.sendMessage(body.groupId, user.id, body.content.trim(), body.lessonId); this.server.to(this.room(body.groupId)).emit('groups:new-message', message); return { ok: true, message }; } private room(groupId: string): string { return `group:${groupId}`; } private async resolveUser(client: Socket) { const authToken = (client.handshake.auth?.token as string | undefined) || ((client.handshake.headers.authorization as string | undefined)?.replace(/^Bearer\s+/i, '')) || undefined; if (!authToken) return null; try { const payload = this.jwtService.verify<{ sub: string; email: string; supabaseId: string }>(authToken); const user = await this.authService.validateJwtPayload(payload); if (user) return user; } catch { // Fallback to Supabase access token } const supabaseUser = await this.supabaseService.verifyToken(authToken); if (!supabaseUser) return null; let user = await this.usersService.findBySupabaseId(supabaseUser.id); if (!user) { user = await this.usersService.create({ supabaseId: supabaseUser.id, email: supabaseUser.email!, name: supabaseUser.user_metadata?.full_name || supabaseUser.user_metadata?.name || null, avatarUrl: supabaseUser.user_metadata?.avatar_url || null, }); } return user; } }