114 lines
3.4 KiB
TypeScript
114 lines
3.4 KiB
TypeScript
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<void> {
|
|
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;
|
|
}
|
|
}
|