From e96faabd7618ff935835950f6638308a089f6d54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Wed, 27 Aug 2025 23:55:01 +0300 Subject: [PATCH] online classroom SignalR --- .../Classroom/IClassroomAppService.cs | 8 +-- .../Classroom/ClassroomAppService.cs | 4 +- .../Classroom/ClassChat.cs | 4 +- .../ClassroomHub.cs | 24 +++++---- .../PlatformHttpApiHostModule.cs | 10 +++- ui/src/proxy/classroom/models.ts | 2 +- ui/src/routes/route.constant.ts | 2 +- ui/src/services/classroom.service.ts | 7 ++- ui/src/services/classroom/signalr.ts | 27 ++++++---- ui/src/services/platformApi.service.ts | 1 + ui/src/views/classroom/ClassList.tsx | 31 ++++++----- ui/src/views/classroom/RoomDetail.tsx | 51 +++++++++++-------- 12 files changed, 102 insertions(+), 69 deletions(-) rename api/src/Kurs.Platform.HttpApi.Host/{VirtualClass => Classroom}/ClassroomHub.cs (88%) diff --git a/api/src/Kurs.Platform.Application.Contracts/Classroom/IClassroomAppService.cs b/api/src/Kurs.Platform.Application.Contracts/Classroom/IClassroomAppService.cs index 5ccec12d..c37f39de 100644 --- a/api/src/Kurs.Platform.Application.Contracts/Classroom/IClassroomAppService.cs +++ b/api/src/Kurs.Platform.Application.Contracts/Classroom/IClassroomAppService.cs @@ -8,13 +8,13 @@ namespace Kurs.Platform.Classrooms; public interface IClassroomAppService : IApplicationService { - Task CreateAsync(ClassroomDto input); - Task> GetListAsync(PagedAndSortedResultRequestDto input); Task GetAsync(Guid id); + Task> GetListAsync(PagedAndSortedResultRequestDto input); + Task CreateAsync(ClassroomDto input); Task UpdateAsync(Guid id, ClassroomDto input); Task DeleteAsync(Guid id); - // Task StartClassAsync(Guid id); - // Task EndClassAsync(Guid id); + Task StartClassAsync(Guid id); + Task EndClassAsync(Guid id); Task JoinClassAsync(Guid id); Task LeaveClassAsync(Guid id); Task> GetAttendanceAsync(Guid sessionId); diff --git a/api/src/Kurs.Platform.Application/Classroom/ClassroomAppService.cs b/api/src/Kurs.Platform.Application/Classroom/ClassroomAppService.cs index 579282f9..c565f1e4 100644 --- a/api/src/Kurs.Platform.Application/Classroom/ClassroomAppService.cs +++ b/api/src/Kurs.Platform.Application/Classroom/ClassroomAppService.cs @@ -121,7 +121,7 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService } [HttpPut] - public async Task StartClassroomAsync(Guid id) + public async Task StartClassAsync(Guid id) { var classSession = await _classSessionRepository.GetAsync(id); @@ -147,7 +147,7 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService } [HttpPut] - public async Task EndClassroomAsync(Guid id) + public async Task EndClassAsync(Guid id) { var classSession = await _classSessionRepository.GetAsync(id); diff --git a/api/src/Kurs.Platform.Domain/Classroom/ClassChat.cs b/api/src/Kurs.Platform.Domain/Classroom/ClassChat.cs index b6e07c6f..eede4a86 100644 --- a/api/src/Kurs.Platform.Domain/Classroom/ClassChat.cs +++ b/api/src/Kurs.Platform.Domain/Classroom/ClassChat.cs @@ -6,7 +6,7 @@ namespace Kurs.Platform.Entities; public class ClassChat : FullAuditedEntity { public Guid SessionId { get; set; } - public Guid SenderId { get; set; } + public Guid? SenderId { get; set; } public string SenderName { get; set; } public string Message { get; set; } public DateTime Timestamp { get; set; } @@ -22,7 +22,7 @@ public class ClassChat : FullAuditedEntity public ClassChat( Guid id, Guid sessionId, - Guid senderId, + Guid? senderId, string senderName, string message, bool isTeacher diff --git a/api/src/Kurs.Platform.HttpApi.Host/VirtualClass/ClassroomHub.cs b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs similarity index 88% rename from api/src/Kurs.Platform.HttpApi.Host/VirtualClass/ClassroomHub.cs rename to api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs index d2337deb..aa4cf320 100644 --- a/api/src/Kurs.Platform.HttpApi.Host/VirtualClass/ClassroomHub.cs +++ b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs @@ -6,6 +6,7 @@ using Volo.Abp.Domain.Repositories; using Kurs.Platform.Entities; using Microsoft.Extensions.Logging; using Volo.Abp.Guids; +using Volo.Abp.Users; namespace Kurs.Platform.SignalR.Hubs; @@ -17,21 +18,25 @@ public class ClassroomHub : Hub private readonly IRepository _chatMessageRepository; private readonly ILogger _logger; private readonly IGuidGenerator _guidGenerator; + private readonly ICurrentUser _currentUser; public ClassroomHub( IRepository classSessionRepository, IRepository participantRepository, IRepository chatMessageRepository, ILogger logger, - IGuidGenerator guidGenerator) + IGuidGenerator guidGenerator, + ICurrentUser currentUser) { _classSessionRepository = classSessionRepository; _participantRepository = participantRepository; _chatMessageRepository = chatMessageRepository; _logger = logger; _guidGenerator = guidGenerator; + _currentUser = currentUser; } + [HubMethodName("JoinClass")] public async Task JoinClassAsync(Guid sessionId, string userName) { var classSession = await _classSessionRepository.GetAsync(sessionId); @@ -47,7 +52,7 @@ public class ClassroomHub : Hub // Update participant connection var participant = await _participantRepository.FirstOrDefaultAsync( - x => x.SessionId == sessionId && x.UserId == Context.UserIdentifier.To() + x => x.SessionId == sessionId && x.UserId == _currentUser.Id ); if (participant != null) @@ -58,16 +63,17 @@ public class ClassroomHub : Hub // Notify others await Clients.Group(sessionId.ToString()) - .SendAsync("ParticipantJoined", Context.UserIdentifier, userName); + .SendAsync("ParticipantJoined", _currentUser.Id, userName); _logger.LogInformation($"User {userName} joined class {sessionId}"); } + [HubMethodName("LeaveClass")] public async Task LeaveClassAsync(Guid sessionId) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, sessionId.ToString()); await Clients.Group(sessionId.ToString()) - .SendAsync("ParticipantLeft", Context.UserIdentifier); - _logger.LogInformation($"User {Context.UserIdentifier} left class {sessionId}"); + .SendAsync("ParticipantLeft", _currentUser); + _logger.LogInformation($"User {_currentUser} left class {sessionId}"); } public async Task SendSignalingMessageAsync(SignalingMessageDto message) @@ -80,8 +86,8 @@ public class ClassroomHub : Hub public async Task SendChatMessageAsync(Guid sessionId, string message) { - var userId = Context.UserIdentifier.To(); - var userName = Context.User?.Identity?.Name ?? "Unknown"; + var userName = _currentUser.UserName; + var userId = _currentUser.Id; // Check if user is teacher var participant = await _participantRepository.FirstOrDefaultAsync( @@ -118,7 +124,7 @@ public class ClassroomHub : Hub public async Task MuteParticipantAsync(Guid sessionId, Guid participantId, bool isMuted) { var teacherParticipant = await _participantRepository.FirstOrDefaultAsync( - x => x.SessionId == sessionId && x.UserId == Context.UserIdentifier.To() + x => x.SessionId == sessionId && x.UserId == _currentUser.Id ); if (teacherParticipant?.IsTeacher != true) @@ -149,7 +155,7 @@ public class ClassroomHub : Hub public override async Task OnDisconnectedAsync(Exception exception) { // Handle cleanup when user disconnects - var userId = Context.UserIdentifier?.To(); + var userId = _currentUser.Id; if (userId.HasValue) { var participants = await _participantRepository.GetListAsync( diff --git a/api/src/Kurs.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs b/api/src/Kurs.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs index 8819494a..b9e36956 100644 --- a/api/src/Kurs.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs +++ b/api/src/Kurs.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs @@ -11,6 +11,7 @@ using Kurs.Platform.BlobStoring; using Kurs.Platform.EntityFrameworkCore; using Kurs.Platform.Extensions; using Kurs.Platform.Identity; +using Kurs.Platform.SignalR.Hubs; using Kurs.Settings; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors; @@ -112,6 +113,8 @@ public class PlatformHttpApiHostModule : AbpModule ConfigureHangfire(context, configuration); ConfigureBlobStoring(configuration); + context.Services.AddSignalR(); + Configure(options => { options.Map(AppErrorCodes.NoAuth, System.Net.HttpStatusCode.Unauthorized); @@ -329,7 +332,7 @@ public class PlatformHttpApiHostModule : AbpModule fileSystem.BasePath = configuration["App:CdnPath"]; }); }); - + options.Containers.Configure(container => { container.UseFileSystem(fileSystem => @@ -400,6 +403,11 @@ public class PlatformHttpApiHostModule : AbpModule AsyncAuthorization = [new AbpHangfireAuthorizationFilter()] }); } + app.UseEndpoints(endpoints => + { + endpoints.MapHub("/classroomhub"); + }); + app.UseConfiguredEndpoints(); } } diff --git a/ui/src/proxy/classroom/models.ts b/ui/src/proxy/classroom/models.ts index 69b1218d..2e038776 100644 --- a/ui/src/proxy/classroom/models.ts +++ b/ui/src/proxy/classroom/models.ts @@ -10,7 +10,7 @@ export interface User { } export interface ClassroomDto { - id?: string + id: string name: string description?: string subject?: string diff --git a/ui/src/routes/route.constant.ts b/ui/src/routes/route.constant.ts index d6134edc..57c201fa 100644 --- a/ui/src/routes/route.constant.ts +++ b/ui/src/routes/route.constant.ts @@ -79,7 +79,7 @@ export const ROUTES_ENUM = { classroom: { dashboard: '/admin/classroom/dashboard', classes: '/admin/classroom/classes', - classroom: '/admin/classroom/room/:id', + roomDetail: '/admin/classroom/room/:id', }, }, accessDenied: '/admin/access-denied', diff --git a/ui/src/services/classroom.service.ts b/ui/src/services/classroom.service.ts index 80fa961a..873c8934 100644 --- a/ui/src/services/classroom.service.ts +++ b/ui/src/services/classroom.service.ts @@ -5,8 +5,7 @@ import { PagedAndSortedResultRequestDto, PagedResultDto } from '@/proxy' export const getClassroomById = (id: string) => apiService.fetchData({ method: 'GET', - url: `/api/app/classroom`, - params: { id }, + url: `/api/app/classroom/${id}`, }) export const getClassrooms = (input: PagedAndSortedResultRequestDto) => @@ -39,11 +38,11 @@ export const deleteClassroom = (id: string) => export const startClassroom = (id: string) => apiService.fetchData({ method: 'PUT', - url: `/api/app/${id}/start-classroom`, + url: `/api/app/classroom/${id}/start-class`, }) export const endClassroom = (id: string) => apiService.fetchData({ method: 'PUT', - url: `/api/app/${id}/end-classroom`, + url: `/api/app/classroom/${id}/end-class`, }) diff --git a/ui/src/services/classroom/signalr.ts b/ui/src/services/classroom/signalr.ts index 83ae0acf..f1a8284a 100644 --- a/ui/src/services/classroom/signalr.ts +++ b/ui/src/services/classroom/signalr.ts @@ -4,6 +4,7 @@ import { HandRaiseDto, SignalingMessageDto, } from '@/proxy/classroom/models' +import { store } from '@/store/store' import * as signalR from '@microsoft/signalr' export class SignalRService { @@ -20,15 +21,17 @@ export class SignalRService { private onHandRaiseDismissed?: (handRaiseId: string) => void constructor() { + const { auth } = store.getState() + // Only initialize connection if not in demo mode if (!this.demoMode) { // In production, replace with your actual SignalR hub URL this.connection = new signalR.HubConnectionBuilder() - .withUrl('https://localhost:5001/classroomhub', { - skipNegotiation: true, - transport: signalR.HttpTransportType.WebSockets, + .withUrl('https://localhost:44344/classroomhub', { + accessTokenFactory: () => auth.session.token || '', }) .withAutomaticReconnect() + .configureLogging(signalR.LogLevel.Information) .build() this.setupEventHandlers() @@ -77,6 +80,10 @@ export class SignalRService { this.connection.onclose(() => { console.log('SignalR connection closed') }) + + this.connection.on('Error', (message: string) => { + console.error('Hub error:', message) + }) } async start(): Promise { @@ -98,7 +105,7 @@ export class SignalRService { } } - async joinClass(sessionId: string, userId: string, userName: string): Promise { + async joinClass(sessionId: string, userName: string): Promise { if (this.demoMode || !this.isConnected) { console.log('Demo mode: Simulating join class for', userName) // Simulate successful join in demo mode @@ -107,24 +114,26 @@ export class SignalRService { } try { - await this.connection.invoke('JoinClass', sessionId, userId, userName) + await this.connection.invoke('JoinClass', sessionId, userName) } catch (error) { console.error('Error joining class:', error) } } - async leaveClass(sessionId: string, userId: string): Promise { + async leaveClass(sessionId: string): Promise { + const { auth } = store.getState() + if (this.demoMode || !this.isConnected) { - console.log('Demo mode: Simulating leave class for user', userId) + console.log('Demo mode: Simulating leave class for user', auth.user.id) // Simulate successful leave in demo mode setTimeout(() => { - this.onParticipantLeft?.(userId) + this.onParticipantLeft?.(auth.user.id) }, 100) return } try { - await this.connection.invoke('LeaveClass', sessionId, userId) + await this.connection.invoke('LeaveClass', sessionId) } catch (error) { console.error('Error leaving class:', error) } diff --git a/ui/src/services/platformApi.service.ts b/ui/src/services/platformApi.service.ts index a921ef79..316751b0 100644 --- a/ui/src/services/platformApi.service.ts +++ b/ui/src/services/platformApi.service.ts @@ -76,6 +76,7 @@ platformApiService.interceptors.response.use( email: tokenDetails?.email, authority: [tokenDetails?.role], name: `${tokenDetails?.given_name} ${tokenDetails?.family_name}`.trim(), + role: 'teacher', }, }) setIsRefreshing(false) diff --git a/ui/src/views/classroom/ClassList.tsx b/ui/src/views/classroom/ClassList.tsx index 83923ab7..eb8cec4d 100644 --- a/ui/src/views/classroom/ClassList.tsx +++ b/ui/src/views/classroom/ClassList.tsx @@ -98,7 +98,7 @@ const ClassList: React.FC = () => { setClassroom(newClassEntity) if (classroom.id) { - navigate(ROUTES_ENUM.protected.admin.classroom.classroom.replace(':id', classroom.id)) + handleJoinClass(classroom) } } catch (error) { console.error('Sınıf oluştururken hata oluştu:', error) @@ -152,8 +152,12 @@ const ClassList: React.FC = () => { await startClassroom(classSession.id!) getClassroomList() + handleJoinClass(classSession) + } + + const handleJoinClass = (classSession: ClassroomDto) => { if (classSession.id) { - navigate(ROUTES_ENUM.protected.admin.classroom.classroom.replace(':id', classSession.id)) + navigate(ROUTES_ENUM.protected.admin.classroom.roomDetail.replace(':id', classSession.id)) } } @@ -313,16 +317,21 @@ const ClassList: React.FC = () => { +