From 9d5ed34ec676c1ae7d534fe5833f39332e3608e9 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 25 Mar 2021 20:47:58 +0100 Subject: [PATCH 1/2] added check endpoint and fixed gui endpoint --- .../ethz/seb/sebserver/WebSecurityConfig.java | 16 ++- ...InstitutionalAuthenticationEntryPoint.java | 107 +++++++++--------- .../dao/impl/RemoteProctoringRoomDAOImpl.java | 2 +- .../weblayer/api/ExamAPI_V1_Controller.java | 29 ++++- .../api/ExamProctoringController.java | 12 +- .../resources/config/application.properties | 2 +- 6 files changed, 105 insertions(+), 63 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java index f62042b7..c2b5d8f4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java @@ -40,6 +40,9 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; @Order(7) public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements ErrorController { + private static final String ERROR_PATH = "/sebserver/error"; + private static final String CHECK_PATH = "/sebserver/check"; + @Value("${sebserver.webservice.http.redirect.gui}") private String guiRedirect; @Value("${sebserver.webservice.api.exam.endpoint.discovery}") @@ -78,22 +81,27 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E public void configure(final WebSecurity web) { web .ignoring() - .antMatchers("/error") + .antMatchers(ERROR_PATH) + .antMatchers(CHECK_PATH) .antMatchers(this.examAPIDiscoveryEndpoint) .antMatchers(this.adminAPIEndpoint + API.INFO_ENDPOINT + API.LOGO_PATH_SEGMENT + "/**") .antMatchers(this.adminAPIEndpoint + API.INFO_ENDPOINT + API.INFO_INST_PATH_SEGMENT + "/**") .antMatchers(this.adminAPIEndpoint + API.REGISTER_ENDPOINT); } - @RequestMapping("/error") + @RequestMapping(CHECK_PATH) + public void check() throws IOException { + } + + @RequestMapping(ERROR_PATH) public void handleError(final HttpServletResponse response) throws IOException { - //response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); + response.getOutputStream().print(response.getStatus()); response.setHeader(HttpHeaders.LOCATION, this.guiRedirect); response.flushBuffer(); } @Override public String getErrorPath() { - return "/error"; + return ERROR_PATH; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java b/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java index 39586d96..14d264bd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java @@ -116,60 +116,56 @@ public final class InstitutionalAuthenticationEntryPoint implements Authenticati final String institutionalEndpoint = extractInstitutionalEndpoint(request); - if (StringUtils.isNoneBlank(institutionalEndpoint) && log.isDebugEnabled()) { - log.debug("No default gui entrypoint requested: {}", institutionalEndpoint); - } else { - request.getSession().setAttribute(INST_SUFFIX_ATTRIBUTE, null); - request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE); - forwardToEntryPoint(request, response, this.guiEntryPoint, false); - return; - } - - try { - - final RestTemplate restTemplate = new RestTemplate(); - final List institutions = restTemplate - .exchange( - this.webserviceURIService.getURIBuilder() - .path(API.INFO_ENDPOINT + API.INFO_INST_ENDPOINT) - .toUriString(), - HttpMethod.GET, - HttpEntity.EMPTY, - new ParameterizedTypeReference>() { - }, - institutionalEndpoint, - API.INFO_PARAM_INST_SUFFIX, - institutionalEndpoint) - .getBody(); - - if (institutions != null && !institutions.isEmpty()) { - request.getSession().setAttribute( - INST_SUFFIX_ATTRIBUTE, - StringUtils.isNotBlank(institutionalEndpoint) - ? institutionalEndpoint - : null); - - if (log.isDebugEnabled()) { - log.debug("Known and active gui entrypoint requested: {}", institutions); - } - - final String logoImageBase64 = requestLogoImage(institutionalEndpoint); - if (StringUtils.isNotBlank(logoImageBase64)) { - request.getSession().setAttribute(API.PARAM_LOGO_IMAGE, logoImageBase64); - - } - forwardToEntryPoint(request, response, this.guiEntryPoint, false); - return; + if (StringUtils.isNotBlank(institutionalEndpoint)) { + if (log.isDebugEnabled()) { + log.debug("No default gui entrypoint requested: {}", institutionalEndpoint); } - } catch (final Exception e) { - log.error("Failed to extract and set institutional endpoint request: ", e); + try { + + final RestTemplate restTemplate = new RestTemplate(); + final List institutions = restTemplate + .exchange( + this.webserviceURIService.getURIBuilder() + .path(API.INFO_ENDPOINT + API.INFO_INST_ENDPOINT) + .toUriString(), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + institutionalEndpoint, + API.INFO_PARAM_INST_SUFFIX, + institutionalEndpoint) + .getBody(); + + if (institutions != null && !institutions.isEmpty()) { + request.getSession().setAttribute( + INST_SUFFIX_ATTRIBUTE, + StringUtils.isNotBlank(institutionalEndpoint) + ? institutionalEndpoint + : null); + + if (log.isDebugEnabled()) { + log.debug("Known and active gui entrypoint requested: {}", institutions); + } + + final String logoImageBase64 = requestLogoImage(institutionalEndpoint); + if (StringUtils.isNotBlank(logoImageBase64)) { + request.getSession().setAttribute(API.PARAM_LOGO_IMAGE, logoImageBase64); + + } + forwardToEntryPoint(request, response, this.guiEntryPoint, false); + return; + } + } catch (final Exception e) { + log.error("Failed to extract and set institutional endpoint request: ", e); + } } request.getSession().setAttribute(INST_SUFFIX_ATTRIBUTE, null); request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE); response.setStatus(HttpStatus.UNAUTHORIZED.value()); - forwardToEntryPoint(request, response, this.guiEntryPoint, true); + forwardToEntryPoint(request, response, this.guiEntryPoint, institutionalEndpoint == null); } @@ -203,16 +199,25 @@ public final class InstitutionalAuthenticationEntryPoint implements Authenticati public static String extractInstitutionalEndpoint(final HttpServletRequest request) { final String requestURI = request.getRequestURI(); - if (StringUtils.isBlank(requestURI) || requestURI.equals(Constants.SLASH.toString())) { + if (StringUtils.isBlank(requestURI)) { return null; } - if (log.isDebugEnabled()) { - log.debug("Trying to verify institution from requested entrypoint url: {}", requestURI); + if (requestURI.equals(Constants.SLASH.toString())) { + return StringUtils.EMPTY; } try { - return requestURI.substring(requestURI.lastIndexOf(Constants.SLASH) + 1); + if (log.isDebugEnabled()) { + log.debug("Trying to verify institution from requested entrypoint url: {}", requestURI); + } + + final String[] split = StringUtils.split(requestURI, Constants.SLASH); + if (split.length > 1) { + return null; + } + + return split[0]; } catch (final Exception e) { log.error("Failed to extract institutional URL suffix: {}", e.getMessage()); return null; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java index c0ebb6a5..76274c48 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java @@ -102,7 +102,7 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { .execute(); if (active > 0) { - throw new IllegalStateException("Townhall, for exam: " + examId + " already existis"); + throw new IllegalStateException("Townhall, for exam: " + examId + " already exists"); } final String newCollectingRoomName = UUID.randomUUID().toString(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java index e0367431..b55e74b6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java @@ -21,6 +21,7 @@ import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; @@ -35,6 +36,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.JSONMapper; @@ -100,7 +102,7 @@ public class ExamAPI_V1_Controller { final POSTMapper mapper = new POSTMapper(formParams, request.getQueryString()); - final String remoteAddr = request.getRemoteAddr(); + final String remoteAddr = this.getClientAddress(request); final Long institutionId = (instIdRequestParam != null) ? instIdRequestParam : mapper.getLong(API.PARAM_INSTITUTION_ID); @@ -158,7 +160,7 @@ public class ExamAPI_V1_Controller { return CompletableFuture.runAsync( () -> { - final String remoteAddr = request.getRemoteAddr(); + final String remoteAddr = this.getClientAddress(request); final Long institutionId = getInstitutionId(principal); if (log.isDebugEnabled()) { @@ -198,7 +200,7 @@ public class ExamAPI_V1_Controller { return CompletableFuture.runAsync( () -> { - final String remoteAddr = request.getRemoteAddr(); + final String remoteAddr = this.getClientAddress(request); final Long institutionId = getInstitutionId(principal); if (log.isDebugEnabled()) { @@ -234,7 +236,7 @@ public class ExamAPI_V1_Controller { return CompletableFuture.runAsync( () -> { - final String remoteAddr = request.getRemoteAddr(); + final String remoteAddr = this.getClientAddress(request); final Long institutionId = getInstitutionId(principal); if (log.isDebugEnabled()) { @@ -419,4 +421,23 @@ public class ExamAPI_V1_Controller { } } + private String getClientAddress(final HttpServletRequest request) { + try { + final String ipAddress = request.getHeader("X-FORWARDED-FOR"); + + if (ipAddress == null) { + return request.getRemoteAddr(); + } + + if (ipAddress.contains(",")) { + return StringUtils.split(ipAddress, Constants.COMMA)[0]; + } + + return ipAddress; + } catch (final Exception e) { + log.warn("Failed to verify client IP address: {}", e.getMessage()); + return request.getHeader("X-FORWARDED-FOR"); + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java index 5e9e2381..698b2e9c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java @@ -316,7 +316,7 @@ public class ExamProctoringController { defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, @PathVariable(name = API.PARAM_MODEL_ID) final Long examId) { - checkAccess(institutionId, examId); + checkExamReadAccess(institutionId); return this.examProcotringRoomService.getTownhallRoomData(examId) .getOrElse(() -> RemoteProctoringRoom.NULL_ROOM); @@ -351,7 +351,8 @@ public class ExamProctoringController { // first create and register a room to collect all connection of the exam // As long as the room exists new connections will join this room immediately // after have been applied to the default collecting room - final RemoteProctoringRoom townhallRoom = this.examProcotringRoomService.createTownhallRoom(examId, subject) + final RemoteProctoringRoom townhallRoom = this.examProcotringRoomService + .createTownhallRoom(examId, subject) .onError(error -> this.examProcotringRoomService.disposeTownhallRoom(examId)) .getOrThrow(); @@ -590,6 +591,13 @@ public class ExamProctoringController { true); } + private void checkExamReadAccess(final Long institutionId) { + this.authorization.check( + PrivilegeType.READ, + EntityType.EXAM, + institutionId); + } + private void checkAccess(final Long institutionId, final Long examId) { this.authorization.check( PrivilegeType.READ, diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index 023e512e..aa543423 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -36,7 +36,7 @@ logging.level.ch=INFO ### spring actuator configuration management.endpoints.web.base-path=/mprofile -management.endpoints.web.exposure.include=metrics,logfile,loggers,heapdump +management.endpoints.web.exposure.include=metrics,logfile,loggers,heapdump,health ########################################################## ### Overall Security Settings From 8470e3b16044a00f42e3df5ca41fb933651d3986 Mon Sep 17 00:00:00 2001 From: anhefti Date: Mon, 29 Mar 2021 13:50:10 +0200 Subject: [PATCH 2/2] SEBSERV-139 single used town-hall and code-cleanup --- .../ch/ethz/seb/sebserver/gbl/api/API.java | 1 + .../gui/content/MonitoringRunningExam.java | 296 +++++++++++------- .../page/impl/JitsiMeetProctoringView.java | 23 +- .../api/session/IsTownhallRoomAvailable.java | 41 +++ .../service/session/ProctoringGUIService.java | 5 +- .../dao/RemoteProctoringRoomDAO.java | 2 + .../dao/impl/RemoteProctoringRoomDAOImpl.java | 31 +- .../session/ExamProctoringRoomService.java | 6 + .../impl/ExamProctoringRoomServiceImpl.java | 5 + .../api/ExamProctoringController.java | 16 + 10 files changed, 290 insertions(+), 136 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/IsTownhallRoomAvailable.java diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 585feb6d..c98d41a8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -186,6 +186,7 @@ public final class API { public static final String EXAM_PROCTORING_ACTIVATE_TOWNHALL_ROOM = "activate-towhall-room"; public static final String EXAM_PROCTORING_DEACTIVATE_TOWNHALL_ROOM = "deactivate-towhall-room"; public static final String EXAM_PROCTORING_TOWNHALL_ROOM_DATA = "towhall-room-data"; + public static final String EXAM_PROCTORING_TOWNHALL_ROOM_AVAILABLE = "towhall-available"; public static final String SEB_CLIENT_CONNECTION_ENDPOINT = "/seb-client-connection"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java index 31384816..7d14170f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java @@ -17,6 +17,7 @@ import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Function; +import org.apache.commons.lang3.BooleanUtils; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; import org.eclipse.swt.SWT; @@ -78,6 +79,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClient import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProcotringRooms; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnectionData; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetTownhallRoom; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.IsTownhallRoomAvailable; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable; import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor; @@ -123,6 +125,7 @@ public class MonitoringRunningExam implements TemplateComposer { private final ServerPushService serverPushService; private final PageService pageService; + private final RestService restService; private final ResourceService resourceService; private final AsyncRunner asyncRunner; private final InstructionProcessor instructionProcessor; @@ -147,6 +150,7 @@ public class MonitoringRunningExam implements TemplateComposer { this.serverPushService = serverPushService; this.pageService = pageService; + this.restService = pageService.getRestService(); this.resourceService = pageService.getResourceService(); this.asyncRunner = asyncRunner; this.instructionProcessor = instructionProcessor; @@ -214,13 +218,14 @@ public class MonitoringRunningExam implements TemplateComposer { this.serverPushService.runServerPush( new ServerPushContext( - content, Utils.truePredicate(), + content, + Utils.truePredicate(), createServerPushUpdateErrorHandler(this.pageService, pageContext)), this.pollInterval, context -> clientTable.updateValues(), updateTableGUI(clientTable)); - final BooleanSupplier privilege = () -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER); + final BooleanSupplier isExamSupporter = () -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER); actionBuilder @@ -242,20 +247,20 @@ public class MonitoringRunningExam implements TemplateComposer { return copyOfPageAction; }) - .publishIf(privilege, false) + .publishIf(isExamSupporter, false) .newAction(ActionDefinition.MONITOR_EXAM_QUIT_ALL) .withEntityKey(entityKey) .withConfirm(() -> CONFIRM_QUIT_ALL) .withExec(action -> this.quitSEBClients(action, clientTable, true)) .noEventPropagation() - .publishIf(privilege) + .publishIf(isExamSupporter) .newAction(ActionDefinition.MONITORING_EXAM_SEARCH_CONNECTIONS) .withEntityKey(entityKey) .withExec(this::openSearchPopup) .noEventPropagation() - .publishIf(privilege) + .publishIf(isExamSupporter) .newAction(ActionDefinition.MONITOR_EXAM_QUIT_SELECTED) .withEntityKey(entityKey) @@ -265,7 +270,7 @@ public class MonitoringRunningExam implements TemplateComposer { action -> this.quitSEBClients(action, clientTable, false), EMPTY_ACTIVE_SELECTION_TEXT_KEY) .noEventPropagation() - .publishIf(privilege, false) + .publishIf(isExamSupporter, false) .newAction(ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION) .withEntityKey(entityKey) @@ -275,81 +280,26 @@ public class MonitoringRunningExam implements TemplateComposer { action -> this.disableSEBClients(action, clientTable, false), EMPTY_SELECTION_TEXT_KEY) .noEventPropagation() - .publishIf(privilege, false); - - if (privilege.getAsBoolean()) { - - if (clientTable.isStatusHidden(ConnectionStatus.CLOSED)) { - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) - .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) - .noEventPropagation() - .withSwitchAction( - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) - .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) - .noEventPropagation() - .create()) - .publish(); - } else { - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) - .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) - .noEventPropagation() - .withSwitchAction( - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) - .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) - .noEventPropagation() - .create()) - .publish(); - } - - if (clientTable.isStatusHidden(ConnectionStatus.CONNECTION_REQUESTED)) { - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION) - .withExec(showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) - .noEventPropagation() - .withSwitchAction( - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION) - .withExec( - hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) - .noEventPropagation() - .create()) - .publish(); - } else { - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION) - .withExec(hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) - .noEventPropagation() - .withSwitchAction( - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION) - .withExec( - showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) - .noEventPropagation() - .create()) - .publish(); - } - - if (clientTable.isStatusHidden(ConnectionStatus.DISABLED)) { - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) - .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) - .noEventPropagation() - .withSwitchAction( - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) - .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) - .noEventPropagation() - .create()) - .publish(); - } else { - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) - .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) - .noEventPropagation() - .withSwitchAction( - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) - .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) - .noEventPropagation() - .create()) - .publish(); - } + .publishIf(isExamSupporter, false); + if (isExamSupporter.getAsBoolean()) { + addFilterActions(actionBuilder, clientTable, isExamSupporter); + addProctoringActions( + currentUser.getProctoringGUIService(), + pageContext, + content, + actionBuilder); } + } - final ProctoringSettings proctoringSettings = restService + private void addProctoringActions( + final ProctoringGUIService proctoringGUIService, + final PageContext pageContext, + final Composite parent, + final PageActionBuilder actionBuilder) { + + final EntityKey entityKey = pageContext.getEntityKey(); + final ProctoringSettings proctoringSettings = this.restService .getBuilder(GetProctoringSettings.class) .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) .call() @@ -359,7 +309,7 @@ public class MonitoringRunningExam implements TemplateComposer { actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM) .withEntityKey(entityKey) - .withExec(this::toggleTownhallRoom) + .withExec(action -> this.toggleTownhallRoom(proctoringGUIService, action)) .noEventPropagation() .publish(); @@ -375,34 +325,126 @@ public class MonitoringRunningExam implements TemplateComposer { final Map> availableRooms = new HashMap<>(); updateRoomActions( - entityKey, pageContext, availableRooms, actionBuilder, - proctoringSettings); + proctoringSettings, + proctoringGUIService); this.serverPushService.runServerPush( new ServerPushContext( - content, + parent, Utils.truePredicate(), createServerPushUpdateErrorHandler(this.pageService, pageContext)), this.proctoringRoomUpdateInterval, context -> updateRoomActions( - entityKey, pageContext, availableRooms, actionBuilder, - proctoringSettings)); + proctoringSettings, + proctoringGUIService)); + } + } + + private void addFilterActions( + final PageActionBuilder actionBuilder, + final ClientConnectionTable clientTable, + final BooleanSupplier isExamSupporter) { + + addClosedFilterAction(actionBuilder, clientTable); + addRequestedFilterAction(actionBuilder, clientTable); + addDisabledFilterAction(actionBuilder, clientTable); + } + + private void addDisabledFilterAction( + final PageActionBuilder actionBuilder, + final ClientConnectionTable clientTable) { + + if (clientTable.isStatusHidden(ConnectionStatus.DISABLED)) { + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) + .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) + .noEventPropagation() + .withSwitchAction( + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) + .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) + .noEventPropagation() + .create()) + .publish(); + } else { + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) + .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) + .noEventPropagation() + .withSwitchAction( + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) + .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) + .noEventPropagation() + .create()) + .publish(); + } + } + + private void addRequestedFilterAction( + final PageActionBuilder actionBuilder, + final ClientConnectionTable clientTable) { + + if (clientTable.isStatusHidden(ConnectionStatus.CONNECTION_REQUESTED)) { + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION) + .withExec(showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) + .noEventPropagation() + .withSwitchAction( + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION) + .withExec( + hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) + .noEventPropagation() + .create()) + .publish(); + } else { + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION) + .withExec(hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) + .noEventPropagation() + .withSwitchAction( + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION) + .withExec( + showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) + .noEventPropagation() + .create()) + .publish(); + } + } + + private void addClosedFilterAction( + final PageActionBuilder actionBuilder, + final ClientConnectionTable clientTable) { + + if (clientTable.isStatusHidden(ConnectionStatus.CLOSED)) { + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) + .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) + .noEventPropagation() + .withSwitchAction( + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) + .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) + .noEventPropagation() + .create()) + .publish(); + } else { + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) + .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) + .noEventPropagation() + .withSwitchAction( + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) + .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) + .noEventPropagation() + .create()) + .publish(); } } private boolean isTownhallRoomActive(final String examModelId) { - final RemoteProctoringRoom townhall = this.pageService.getRestService() - .getBuilder(GetTownhallRoom.class) + return !BooleanUtils.toBoolean(this.pageService + .getRestService() + .getBuilder(IsTownhallRoomAvailable.class) .withURIVariable(API.PARAM_MODEL_ID, examModelId) .call() - .getOr(null); - - return townhall != null && townhall.id != null; + .getOr(Constants.FALSE_STRING)); } private PageAction openSearchPopup(final PageAction action) { @@ -410,9 +452,12 @@ public class MonitoringRunningExam implements TemplateComposer { return action; } - private PageAction toggleTownhallRoom(final PageAction action) { + private PageAction toggleTownhallRoom( + final ProctoringGUIService proctoringGUIService, + final PageAction action) { + if (isTownhallRoomActive(action.getEntityKey().modelId)) { - closeTownhallRoom(action); + closeTownhallRoom(proctoringGUIService, action); this.pageService.firePageEvent( new ActionActivationEvent( true, @@ -422,7 +467,7 @@ public class MonitoringRunningExam implements TemplateComposer { action.pageContext()); return action; } else { - openTownhallRoom(action); + openTownhallRoom(proctoringGUIService, action); this.pageService.firePageEvent( new ActionActivationEvent( true, @@ -434,14 +479,12 @@ public class MonitoringRunningExam implements TemplateComposer { } } - private PageAction openTownhallRoom(final PageAction action) { + private PageAction openTownhallRoom( + final ProctoringGUIService proctoringGUIService, + final PageAction action) { + try { final EntityKey examId = action.getEntityKey(); - - final ProctoringGUIService proctoringGUIService = this.pageService - .getCurrentUser() - .getProctoringGUIService(); - String activeAllRoomName = proctoringGUIService.getTownhallRoom(examId.modelId); if (activeAllRoomName == null) { @@ -476,7 +519,10 @@ public class MonitoringRunningExam implements TemplateComposer { return action; } - private PageAction closeTownhallRoom(final PageAction action) { + private PageAction closeTownhallRoom( + final ProctoringGUIService proctoringGUIService, + final PageAction action) { + final String examId = action.getEntityKey().modelId; final RemoteProctoringRoom townhall = this.pageService.getRestService() .getBuilder(GetTownhallRoom.class) @@ -492,45 +538,59 @@ public class MonitoringRunningExam implements TemplateComposer { } try { - final ProctoringGUIService proctoringGUIService = this.pageService - .getCurrentUser() - .getProctoringGUIService(); - proctoringGUIService.closeRoom(townhall.name); } catch (final Exception e) { - log.error("Failed to close procotring townhall room for exam: {}", examId); + log.error("Failed to close proctoring townhall room for exam: {}", examId); } return action; } - private void updateTownhallButton(final EntityKey entityKey, final PageContext pageContext) { + private void updateTownhallButton( + final ProctoringGUIService proctoringGUIService, + final PageContext pageContext) { + final EntityKey entityKey = pageContext.getEntityKey(); + if (isTownhallRoomActive(entityKey.modelId)) { - this.pageService.firePageEvent( - new ActionActivationEvent( - true, - new Tuple<>( - ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, - ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), - pageContext); + final boolean townhallRoomFromThisUser = proctoringGUIService + .getTownhallRoom(entityKey.modelId) != null; + if (townhallRoomFromThisUser) { + this.pageService.firePageEvent( + new ActionActivationEvent( + true, + new Tuple<>( + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), + pageContext); + } else { + this.pageService.firePageEvent( + new ActionActivationEvent( + false, + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), + pageContext); + } } else { this.pageService.firePageEvent( new ActionActivationEvent( true, new Tuple<>( ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, - ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)), + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM), + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), pageContext); } } private void updateRoomActions( - final EntityKey entityKey, final PageContext pageContext, final Map> rooms, final PageActionBuilder actionBuilder, - final ProctoringSettings proctoringSettings) { + final ProctoringSettings proctoringSettings, + final ProctoringGUIService proctoringGUIService) { - updateTownhallButton(entityKey, pageContext); + final EntityKey entityKey = pageContext.getEntityKey(); + updateTownhallButton(proctoringGUIService, pageContext); final I18nSupport i18nSupport = this.pageService.getI18nSupport(); this.pageService.getRestService().getBuilder(GetProcotringRooms.class) .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) @@ -571,7 +631,7 @@ public class MonitoringRunningExam implements TemplateComposer { this.pageService.publishAction( action, _treeItem -> rooms.put(room.name, new Pair<>(room, _treeItem))); - addRoomConnectionsPopupListener(entityKey, pageContext, rooms); + addRoomConnectionsPopupListener(pageContext, rooms); processProctorRoomActionActivation(rooms.get(room.name).b, room, pageContext); } }); @@ -596,11 +656,11 @@ public class MonitoringRunningExam implements TemplateComposer { } private void addRoomConnectionsPopupListener( - final EntityKey entityKey, final PageContext pageContext, final Map> rooms) { if (!rooms.isEmpty()) { + final EntityKey entityKey = pageContext.getEntityKey(); final TreeItem treeItem = rooms.values().iterator().next().b; final Tree tree = treeItem.getParent(); if (tree.getData(SHOW_CONNECTION_ACTION_APPLIED) == null) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java index aa9ff9a9..e43604c5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java @@ -80,17 +80,18 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { public void compose(final PageContext pageContext) { final ProctoringWindowData proctoringWindowData = ProctoringGUIService.getCurrentProctoringWindowData(); - final Composite parent = pageContext.getParent(); - final Composite content = new Composite(parent, SWT.NONE | SWT.NO_SCROLL); final GridLayout gridLayout = new GridLayout(); + final ProctoringGUIService proctoringGUIService = this.pageService + .getCurrentUser() + .getProctoringGUIService(); content.setLayout(gridLayout); final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true); content.setLayoutData(headerCell); - parent.addListener(SWT.Dispose, event -> closeRoom(proctoringWindowData)); + parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData)); final String url = this.guiServiceInfo .getExternalServerURIBuilder() @@ -120,7 +121,7 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { final Button closeAction = widgetFactory.buttonLocalized(footer, CLOSE_WINDOW_TEXT_KEY); closeAction.setLayoutData(new RowData(150, 30)); - closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringWindowData)); + closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringGUIService, proctoringWindowData)); final BroadcastActionState broadcastActionState = new BroadcastActionState(); final String connectionTokens = getConnectionTokens(proctoringWindowData); @@ -256,11 +257,15 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { boolean chat = false; } - private void closeRoom(final ProctoringWindowData proctoringWindowData) { - this.pageService - .getCurrentUser() - .getProctoringGUIService() - .closeRoom(proctoringWindowData.connectionData.roomName); + private void closeRoom( + final ProctoringGUIService proctoringGUIService, + final ProctoringWindowData proctoringWindowData) { + + try { + proctoringGUIService.closeRoom(proctoringWindowData.connectionData.roomName); + } catch (final Exception e) { + log.error("Failed to close proctoring window properly: ", e); + } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/IsTownhallRoomAvailable.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/IsTownhallRoomAvailable.java new file mode 100644 index 00000000..8aff4063 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/IsTownhallRoomAvailable.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class IsTownhallRoomAvailable extends RestCall { + + public IsTownhallRoomAvailable() { + super(new TypeKey<>( + CallType.GET_SINGLE, + EntityType.REMOTE_PROCTORING_ROOM, + new TypeReference() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_PROCTORING_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_PROCTORING_TOWNHALL_ROOM_AVAILABLE); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java index de5eb00b..8009206f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java @@ -195,7 +195,7 @@ public class ProctoringGUIService { closeWindow(name); final RoomConnectionData roomConnectionData = this.rooms.remove(name); if (roomConnectionData != null) { - // send reset of broadcast attributes to all in the room + // Send reset of broadcast attributes to all in the room this.restService.getBuilder(SendProctoringBroadcastAttributes.class) .withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId) .withFormParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, roomConnectionData.roomName) @@ -209,7 +209,7 @@ public class ProctoringGUIService { "Failed to send reset broadcast attribute instruction call for room: {}, cause: {}", roomConnectionData.roomName, error.getMessage())); - // send instruction to leave this room and join the own exam collecting room + // Send instruction to leave this room and join the own exam collecting room if (!roomConnectionData.connections.isEmpty()) { this.restService.getBuilder(SendRejoinExamCollectionRoom.class) .withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId) @@ -221,6 +221,7 @@ public class ProctoringGUIService { name, error.getMessage())); } else { + // Close town-hall room this.restService.getBuilder(DisposeTownhallRoom.class) .withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId) .call() diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java index 25b8dab6..b4cbe371 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java @@ -23,6 +23,8 @@ public interface RemoteProctoringRoomDAO { Result getRoomName(Long roomId); + boolean isTownhallRoomActive(Long examId); + Result getTownhallRoom(Long examId); Result createTownhallRoom(Long examId, String subject); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java index 76274c48..0a5e278a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java @@ -19,6 +19,8 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -39,6 +41,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; @WebServiceProfile public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { + private static final Logger log = LoggerFactory.getLogger(RemoteProctoringRoomDAOImpl.class); + private final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper; protected RemoteProctoringRoomDAOImpl( @@ -94,14 +98,9 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { @Transactional public Result createTownhallRoom(final Long examId, final String subject) { return Result.tryCatch(() -> { - // check first if town-hall room is not already active - final long active = this.remoteProctoringRoomRecordMapper.countByExample() - .where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId)) - .and(RemoteProctoringRoomRecordDynamicSqlSupport.townhallRoom, isNotEqualTo(0)) - .build() - .execute(); - if (active > 0) { + // Check first if town-hall room is not already active + if (isTownhallRoomActive(examId)) { throw new IllegalStateException("Townhall, for exam: " + examId + " already exists"); } @@ -122,6 +121,24 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { .onError(TransactionHandler::rollback); } + @Override + @Transactional(readOnly = true) + public boolean isTownhallRoomActive(final Long examId) { + try { + final long active = this.remoteProctoringRoomRecordMapper.countByExample() + .where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId)) + .and(RemoteProctoringRoomRecordDynamicSqlSupport.townhallRoom, isNotEqualTo(0)) + .build() + .execute(); + return (active > 0); + } catch (final Exception e) { + log.error( + "Failed to verify town-hall room activity for exam: {}. Mark it as active to avoid double openings", + examId, e); + return true; + } + } + @Override @Transactional public Result saveRoom(final Long examId, final RemoteProctoringRoom room) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java index 80da5551..86d5a83a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java @@ -44,6 +44,12 @@ public interface ExamProctoringRoomService { * name of an exam. */ void updateProctoringCollectingRooms(); + /** Indicates whether the town-hall room for a specified exam is active or not. + * + * @param examId the exam identifier + * @return true if the town-hall room for specified exam is active, false if not. */ + boolean isTownhallRoomActive(Long examId); + /** This creates a town-hall room for a specific exam. The exam must be active and running * and there must be no other town-hall room already be active. An unique room name will be * created and returned. diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java index 3c90f48f..dfcbb8a2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java @@ -95,6 +95,11 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService } } + @Override + public boolean isTownhallRoomActive(final Long examId) { + return this.remoteProctoringRoomDAO.isTownhallRoomActive(examId); + } + @Override public Result createTownhallRoom(final Long examId, final String subject) { if (!this.examSessionService.isExamRunning(examId)) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java index 698b2e9c..902807ea 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java @@ -304,6 +304,22 @@ public class ExamProctoringController { Utils.toByteArray(connectionToken)); } + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_PROCTORING_TOWNHALL_ROOM_AVAILABLE, + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public String isTownhallRoomAvialbale( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @PathVariable(name = API.PARAM_MODEL_ID) final Long examId) { + + checkExamReadAccess(institutionId); + return String.valueOf(!this.examProcotringRoomService.isTownhallRoomActive(examId)); + } + @RequestMapping( path = API.MODEL_ID_VAR_PATH_SEGMENT + API.EXAM_PROCTORING_TOWNHALL_ROOM_DATA,